Skip to content

Commit

Permalink
Support service processes (#69)
Browse files Browse the repository at this point in the history
* Move PhoenixActivity to a separate class

* Add support for restarting a Service.

* Apply suggestions from code review

Co-authored-by: Jake Wharton <[email protected]>

* Split Activity and Service methods, keeping the existing methods Activity-only.

* Run spotless code formatting

---------

Co-authored-by: jobhh <[email protected]>
Co-authored-by: Jake Wharton <[email protected]>
  • Loading branch information
3 people authored Mar 25, 2024
1 parent ceaf29c commit 21907fb
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 32 deletions.
8 changes: 7 additions & 1 deletion process-phoenix/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

<application>
<activity
android:name=".ProcessPhoenix"
android:name=".PhoenixActivity"
android:exported="false"
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
@@ -0,0 +1,37 @@
package com.jakewharton.processphoenix;

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.os.StrictMode;

public final class PhoenixActivity extends Activity {

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

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

Intent[] intents =
getIntent()
.<Intent>getParcelableArrayListExtra(ProcessPhoenix.KEY_RESTART_INTENTS)
.toArray(new Intent[0]);

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());
}

startActivities(intents);
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 final 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 @@ -21,13 +21,12 @@

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;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.os.StrictMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -39,9 +38,10 @@
* <p>
* Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance.
*/
public final class ProcessPhoenix extends Activity {
private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
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";

/**
* Call to restart the application process using the {@linkplain Intent#CATEGORY_DEFAULT default}
Expand All @@ -53,6 +53,16 @@ public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

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

/**
* Call to restart the application process using the specified intents.
* <p>
Expand All @@ -65,14 +75,36 @@ public static void triggerRebirth(Context context, Intent... nextIntents) {
// create a new task for the first activity.
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Intent intent = new Intent(context, ProcessPhoenix.class);
Intent intent = new Intent(context, PhoenixActivity.class);
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
intent.putParcelableArrayListExtra(
KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
context.startActivity(intent);
}

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

/**
* 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 All @@ -95,30 +127,6 @@ private static Intent getRestartIntent(Context context) {
+ ". Does an activity specify the DEFAULT category in its intent filter?");
}

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

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

Intent[] intents =
getIntent().<Intent>getParcelableArrayListExtra(KEY_RESTART_INTENTS).toArray(new Intent[0]);

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());
}

startActivities(intents);
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

/**
* Checks if the current process is a temporary Phoenix Process.
* This can be used to avoid initialisation of unused resources or to prevent running code that
Expand Down
10 changes: 10 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,12 @@
<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 @@ -21,6 +22,8 @@ protected void onCreate(Bundle savedInstanceState) {
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 @@ -42,5 +45,30 @@ 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 final 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();
}
}
Loading

0 comments on commit 21907fb

Please sign in to comment.