Skip to content

Commit

Permalink
Add support for restarting a Service.
Browse files Browse the repository at this point in the history
  • Loading branch information
jobhh committed Mar 1, 2024
1 parent de73594 commit a3ecf29
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 2 deletions.
6 changes: 6 additions & 0 deletions process-phoenix/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@
android:process=":phoenix"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
/>

<service
android:name=".PhoenixService"
android:exported="false"
android:process=":phoenix"
/>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import android.os.StrictMode;

public class PhoenixActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Process.killProcess(getIntent().getIntExtra(ProcessPhoenix.KEY_MAIN_PROCESS_PID, -1)); // Kill original main process
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.jakewharton.processphoenix;

import android.app.IntentService;
import android.content.Intent;
import android.os.Build;
import android.os.Process;
import android.os.StrictMode;

/**
* Please note that restarting a Service multiple times can result in an increasingly long delay between restart times.
* This is a safety mechanism, since Android registers the restart of this service as a crashed service.
* <p>
* The observed delay periods are: 1s, 4s, 16s, 64s, 256s, 1024s. (on an Android 11 device)
* Which seems to follow this pattern: 4^x, with x being the restart attempt minus 1.
*/
public class PhoenixService extends IntentService {

public PhoenixService() {
super("PhoenixService");
}

@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
return;
}

Process.killProcess(intent.getIntExtra(ProcessPhoenix.KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

Intent nextIntent;
if (Build.VERSION.SDK_INT >= 33) {
nextIntent = intent.getParcelableExtra(ProcessPhoenix.KEY_RESTART_INTENT, Intent.class);
} else {
nextIntent = intent.getParcelableExtra(ProcessPhoenix.KEY_RESTART_INTENT);
}

if (Build.VERSION.SDK_INT > 31) {
// Disable strict mode complaining about out-of-process intents. Normally you save and restore
// the original policy, but this process will die almost immediately after the offending call.
StrictMode.setVmPolicy(
new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy())
.permitUnsafeIntentLaunch()
.build());
}

if (Build.VERSION.SDK_INT >= 26) {
startForegroundService(nextIntent);
} else {
startService(nextIntent);
}

Runtime.getRuntime().exit(0); // Kill kill kill!
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import android.app.Activity;
import android.app.ActivityManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
Expand All @@ -40,6 +41,7 @@
* Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance.
*/
public final class ProcessPhoenix {
static final String KEY_RESTART_INTENT = "phoenix_restart_intent";
static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";

Expand All @@ -53,15 +55,60 @@ public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

/**
* Call to restart the application process using the provided targetClass as an intent.
* <p>
* Behavior of the current process after invoking this method is undefined.
*/
public static void triggerRebirth(Context context, Class<?> targetClass) {
Intent nextIntent = new Intent(context, targetClass);

if (Service.class.isAssignableFrom(targetClass)) {
triggerServiceRebirth(context, nextIntent);
return;
}

// Default to Activity rebirth
triggerActivityRebirth(context, nextIntent);
}

/**
* Call to restart the application process using the specified intents.
* Please note: If the intents resolves to a Service only the first intent is used.
* <p>
* Behavior of the current process after invoking this method is undefined.
*/
public static void triggerRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}

Intent firstIntent = nextIntents[0];
PackageManager pm = context.getPackageManager();

if (pm.resolveActivity(firstIntent, 0) != null) {
triggerActivityRebirth(context, nextIntents);
return;
}

if (pm.resolveService(firstIntent, 0) != null) {
triggerServiceRebirth(context, firstIntent);
return;
}

// If the first intent does not resolve to an Activity or Service, default to Activity rebirth
triggerActivityRebirth(context, nextIntents);
}

/**
* Call to restart the application process using the specified intents.
* <p>
* Behavior of the current process after invoking this method is undefined.
*/
public static void triggerActivityRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
// create a new task for the first activity.
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Expand All @@ -72,6 +119,19 @@ public static void triggerRebirth(Context context, Intent... nextIntents) {
context.startActivity(intent);
}


/**
* Call to restart the application process using the specified Service intent.
* <p>
* Behavior of the current process after invoking this method is undefined.
*/
public static void triggerServiceRebirth(Context context, Intent nextIntent) {
Intent intent = new Intent(context, PhoenixService.class);
intent.putExtra(KEY_RESTART_INTENT, nextIntent);
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
context.startService(intent);
}

private static Intent getRestartIntent(Context context) {
String packageName = context.getPackageName();

Expand Down
9 changes: 9 additions & 0 deletions sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
xmlns:tools="http://schemas.android.com/tools"
>

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

<application
android:label="Process Phoenix"
tools:ignore="MissingApplicationIcon"
Expand All @@ -18,5 +21,11 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>


<service
android:name=".RestartService"
android:exported="true"
android:foregroundServiceType="shortService" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.view.View;
Expand All @@ -20,6 +21,8 @@ public final class MainActivity extends Activity {
TextView extraTextView = findViewById(R.id.extra_text);
View restartButton = findViewById(R.id.restart);
View restartWithIntentButton = findViewById(R.id.restart_with_intent);
View restartActivityButton = findViewById(R.id.restartActivity);
View restartServiceButton = findViewById(R.id.restart_service);

processIdView.setText("Process ID: " + Process.myPid());
extraTextView.setText("Extra Text: " + getIntent().getStringExtra(EXTRA_TEXT));
Expand All @@ -39,5 +42,27 @@ public void onClick(View v) {
ProcessPhoenix.triggerRebirth(MainActivity.this, nextIntent);
}
});

restartActivityButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ProcessPhoenix.triggerRebirth(MainActivity.this, MainActivity.class);
}
});

restartServiceButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Start the RestartService, which initiates the service restart cycle.
// TODO: Request permissions when the API level is high enough to require FOREGROUND_SERVICE or POST_NOTIFICATIONS
Intent restartServiceIntent = new Intent(MainActivity.this, RestartService.class);
if (Build.VERSION.SDK_INT >= 26) {
startForegroundService(restartServiceIntent);
} else {
startService(restartServiceIntent);
}
finish();
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.jakewharton.processphoenix.sample;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;

public class NotificationBuilder {

/**
* Create a Notification, required to support Service restarting on Android 8 and newer
*/
@TargetApi(26)
public static Notification createNotification(Context context) {
// Android 13 or higher requires a permission to post Notifications
if (Build.VERSION.SDK_INT >= 33) {
if (context.checkCallingOrSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
Log.e(
"ProcessPhoenix",
"Required POST_NOTIFICATIONS permission was not granted, cannot restart Service"
);
return null;
}
}

// Android 8 or higher requires a Notification Channel
if (Build.VERSION.SDK_INT >= 26) {
// Creating an existing notification channel with its original values performs no operation, so it's safe to call this code multiple times
NotificationChannel channel = new NotificationChannel(
"ProcessPhoenix",
"ProcessPhoenix",
NotificationManager.IMPORTANCE_NONE
);

// Create Notification Channel
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
}

// Create a Notification
return new Notification.Builder(context, "ProcessPhoenix")
.setSmallIcon(android.R.mipmap.sym_def_app_icon)
.setContentTitle("ProcessPhoenix")
.setContentText("PhoenixService")
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.jakewharton.processphoenix.sample;

import android.annotation.SuppressLint;
import android.app.IntentService;
import android.content.Intent;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import com.jakewharton.processphoenix.ProcessPhoenix;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* This example service will attempt to restart after 1 second
* <p>
* Please note that restarting a Service multiple times can result in an increasingly long delay between restart times.
* This is a safety mechanism, since Android registers the restart of this service as a crashed service.
* <p>
* The observed delay periods are: 1s, 4s, 16s, 64s, 256s, 1024s. (on an Android 11 device)
* Which seems to follow this pattern: 4^x, with x being the restart attempt minus 1.
*/
public class RestartService extends IntentService {

public RestartService() {
super("RestartService");
}

@SuppressLint("ForegroundServiceType")
@Override
protected void onHandleIntent(Intent intent) {
// Log something to console to easily track successful restarts
Log.d(
"ProcessPhoenix",
"--- RestartService started with PID: " + Process.myPid() + " ---"
);

if (Build.VERSION.SDK_INT >= 26) {
startForeground(1337, NotificationBuilder.createNotification(RestartService.this));
}

// Trigger rebirth from a separate thread, such that the onStartCommand can finish properly
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore
}
ProcessPhoenix.triggerRebirth(RestartService.this, RestartService.class);
// ProcessPhoenix.triggerRebirth(RestartService.this, new Intent(RestartService.this, RestartService.class));
});
}
}
18 changes: 17 additions & 1 deletion sample/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Restart Process"
android:text="Restart Process with default app launch intent"
/>

<Button
Expand All @@ -41,4 +41,20 @@
android:text="Restart Process with Intent"
/>

<Button
android:id="@+id/restartActivity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Restart Process with MainActivity Class"
/>

<Button
android:id="@+id/restart_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Stop Activity and start service restart loop"
/>

</LinearLayout>

0 comments on commit a3ecf29

Please sign in to comment.