Skip to content

Commit 21907fb

Browse files
jobhhjobhhJakeWharton
authored
Support service processes (#69)
* Move PhoenixActivity to a separate class * Add support for restarting a Service. * Apply suggestions from code review Co-authored-by: Jake Wharton <github@jakewharton.com> * Split Activity and Service methods, keeping the existing methods Activity-only. * Run spotless code formatting --------- Co-authored-by: jobhh <job@dreambyte.nl> Co-authored-by: Jake Wharton <github@jakewharton.com>
1 parent ceaf29c commit 21907fb

File tree

9 files changed

+293
-32
lines changed

9 files changed

+293
-32
lines changed

process-phoenix/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
<application>
44
<activity
5-
android:name=".ProcessPhoenix"
5+
android:name=".PhoenixActivity"
66
android:exported="false"
77
android:process=":phoenix"
88
android:theme="@android:style/Theme.Translucent.NoTitleBar"
99
/>
10+
11+
<service
12+
android:name=".PhoenixService"
13+
android:exported="false"
14+
android:process=":phoenix"
15+
/>
1016
</application>
1117
</manifest>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.jakewharton.processphoenix;
2+
3+
import android.app.Activity;
4+
import android.content.Intent;
5+
import android.os.Build;
6+
import android.os.Bundle;
7+
import android.os.Process;
8+
import android.os.StrictMode;
9+
10+
public final class PhoenixActivity extends Activity {
11+
12+
@Override
13+
protected void onCreate(Bundle savedInstanceState) {
14+
super.onCreate(savedInstanceState);
15+
16+
// Kill original main process
17+
Process.killProcess(getIntent().getIntExtra(ProcessPhoenix.KEY_MAIN_PROCESS_PID, -1));
18+
19+
Intent[] intents =
20+
getIntent()
21+
.<Intent>getParcelableArrayListExtra(ProcessPhoenix.KEY_RESTART_INTENTS)
22+
.toArray(new Intent[0]);
23+
24+
if (Build.VERSION.SDK_INT > 31) {
25+
// Disable strict mode complaining about out-of-process intents. Normally you save and restore
26+
// the original policy, but this process will die almost immediately after the offending call.
27+
StrictMode.setVmPolicy(
28+
new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy())
29+
.permitUnsafeIntentLaunch()
30+
.build());
31+
}
32+
33+
startActivities(intents);
34+
finish();
35+
Runtime.getRuntime().exit(0); // Kill kill kill!
36+
}
37+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.jakewharton.processphoenix;
2+
3+
import android.app.IntentService;
4+
import android.content.Intent;
5+
import android.os.Build;
6+
import android.os.Process;
7+
import android.os.StrictMode;
8+
9+
/**
10+
* Please note that restarting a Service multiple times can result in an increasingly long delay between restart times.
11+
* This is a safety mechanism, since Android registers the restart of this service as a crashed service.
12+
* <p>
13+
* The observed delay periods are: 1s, 4s, 16s, 64s, 256s, 1024s. (on an Android 11 device)
14+
* Which seems to follow this pattern: 4^x, with x being the restart attempt minus 1.
15+
*/
16+
public final class PhoenixService extends IntentService {
17+
18+
public PhoenixService() {
19+
super("PhoenixService");
20+
}
21+
22+
@Override
23+
protected void onHandleIntent(Intent intent) {
24+
if (intent == null) {
25+
return;
26+
}
27+
28+
Process.killProcess(
29+
intent.getIntExtra(ProcessPhoenix.KEY_MAIN_PROCESS_PID, -1)); // Kill original main process
30+
31+
Intent nextIntent;
32+
if (Build.VERSION.SDK_INT >= 33) {
33+
nextIntent = intent.getParcelableExtra(ProcessPhoenix.KEY_RESTART_INTENT, Intent.class);
34+
} else {
35+
nextIntent = intent.getParcelableExtra(ProcessPhoenix.KEY_RESTART_INTENT);
36+
}
37+
38+
if (Build.VERSION.SDK_INT > 31) {
39+
// Disable strict mode complaining about out-of-process intents. Normally you save and restore
40+
// the original policy, but this process will die almost immediately after the offending call.
41+
StrictMode.setVmPolicy(
42+
new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy())
43+
.permitUnsafeIntentLaunch()
44+
.build());
45+
}
46+
47+
if (Build.VERSION.SDK_INT >= 26) {
48+
startForegroundService(nextIntent);
49+
} else {
50+
startService(nextIntent);
51+
}
52+
53+
Runtime.getRuntime().exit(0); // Kill kill kill!
54+
}
55+
}

process-phoenix/src/main/java/com/jakewharton/processphoenix/ProcessPhoenix.java

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@
2121

2222
import android.app.Activity;
2323
import android.app.ActivityManager;
24+
import android.app.Service;
2425
import android.content.Context;
2526
import android.content.Intent;
2627
import android.content.pm.PackageManager;
2728
import android.os.Build;
28-
import android.os.Bundle;
2929
import android.os.Process;
30-
import android.os.StrictMode;
3130
import java.util.ArrayList;
3231
import java.util.Arrays;
3332
import java.util.List;
@@ -39,9 +38,10 @@
3938
* <p>
4039
* Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance.
4140
*/
42-
public final class ProcessPhoenix extends Activity {
43-
private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
44-
private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
41+
public final class ProcessPhoenix {
42+
static final String KEY_RESTART_INTENT = "phoenix_restart_intent";
43+
static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
44+
static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
4545

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

56+
/**
57+
* Call to restart the application process using the provided Activity Class.
58+
* <p>
59+
* Behavior of the current process after invoking this method is undefined.
60+
*/
61+
public static void triggerRebirth(Context context, Class<? extends Activity> targetClass) {
62+
Intent nextIntent = new Intent(context, targetClass);
63+
triggerRebirth(context, nextIntent);
64+
}
65+
5666
/**
5767
* Call to restart the application process using the specified intents.
5868
* <p>
@@ -65,14 +75,36 @@ public static void triggerRebirth(Context context, Intent... nextIntents) {
6575
// create a new task for the first activity.
6676
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
6777

68-
Intent intent = new Intent(context, ProcessPhoenix.class);
78+
Intent intent = new Intent(context, PhoenixActivity.class);
6979
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
7080
intent.putParcelableArrayListExtra(
7181
KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
7282
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
7383
context.startActivity(intent);
7484
}
7585

86+
/**
87+
* Call to restart the application process using the provided Service Class.
88+
* <p>
89+
* Behavior of the current process after invoking this method is undefined.
90+
*/
91+
public static void triggerServiceRebirth(Context context, Class<? extends Service> targetClass) {
92+
Intent nextIntent = new Intent(context, targetClass);
93+
triggerServiceRebirth(context, nextIntent);
94+
}
95+
96+
/**
97+
* Call to restart the application process using the specified Service intent.
98+
* <p>
99+
* Behavior of the current process after invoking this method is undefined.
100+
*/
101+
public static void triggerServiceRebirth(Context context, Intent nextIntent) {
102+
Intent intent = new Intent(context, PhoenixService.class);
103+
intent.putExtra(KEY_RESTART_INTENT, nextIntent);
104+
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
105+
context.startService(intent);
106+
}
107+
76108
private static Intent getRestartIntent(Context context) {
77109
String packageName = context.getPackageName();
78110

@@ -95,30 +127,6 @@ private static Intent getRestartIntent(Context context) {
95127
+ ". Does an activity specify the DEFAULT category in its intent filter?");
96128
}
97129

98-
@Override
99-
protected void onCreate(Bundle savedInstanceState) {
100-
super.onCreate(savedInstanceState);
101-
102-
Process.killProcess(
103-
getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process
104-
105-
Intent[] intents =
106-
getIntent().<Intent>getParcelableArrayListExtra(KEY_RESTART_INTENTS).toArray(new Intent[0]);
107-
108-
if (Build.VERSION.SDK_INT > 31) {
109-
// Disable strict mode complaining about out-of-process intents. Normally you save and restore
110-
// the original policy, but this process will die almost immediately after the offending call.
111-
StrictMode.setVmPolicy(
112-
new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy())
113-
.permitUnsafeIntentLaunch()
114-
.build());
115-
}
116-
117-
startActivities(intents);
118-
finish();
119-
Runtime.getRuntime().exit(0); // Kill kill kill!
120-
}
121-
122130
/**
123131
* Checks if the current process is a temporary Phoenix Process.
124132
* This can be used to avoid initialisation of unused resources or to prevent running code that

sample/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
xmlns:tools="http://schemas.android.com/tools"
44
>
55

6+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
7+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
8+
69
<application
710
android:label="Process Phoenix"
811
tools:ignore="MissingApplicationIcon"
@@ -18,5 +21,12 @@
1821
<category android:name="android.intent.category.DEFAULT" />
1922
</intent-filter>
2023
</activity>
24+
25+
26+
<service
27+
android:name=".RestartService"
28+
android:exported="true"
29+
android:foregroundServiceType="shortService"
30+
/>
2131
</application>
2232
</manifest>

sample/src/main/java/com/jakewharton/processphoenix/sample/MainActivity.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import android.app.Activity;
44
import android.content.Intent;
5+
import android.os.Build;
56
import android.os.Bundle;
67
import android.os.Process;
78
import android.view.View;
@@ -21,6 +22,8 @@ protected void onCreate(Bundle savedInstanceState) {
2122
TextView extraTextView = findViewById(R.id.extra_text);
2223
View restartButton = findViewById(R.id.restart);
2324
View restartWithIntentButton = findViewById(R.id.restart_with_intent);
25+
View restartActivityButton = findViewById(R.id.restartActivity);
26+
View restartServiceButton = findViewById(R.id.restart_service);
2427

2528
processIdView.setText("Process ID: " + Process.myPid());
2629
extraTextView.setText("Extra Text: " + getIntent().getStringExtra(EXTRA_TEXT));
@@ -42,5 +45,30 @@ public void onClick(View v) {
4245
ProcessPhoenix.triggerRebirth(MainActivity.this, nextIntent);
4346
}
4447
});
48+
49+
restartActivityButton.setOnClickListener(
50+
new View.OnClickListener() {
51+
@Override
52+
public void onClick(View v) {
53+
ProcessPhoenix.triggerRebirth(MainActivity.this, MainActivity.class);
54+
}
55+
});
56+
57+
restartServiceButton.setOnClickListener(
58+
new View.OnClickListener() {
59+
@Override
60+
public void onClick(View v) {
61+
// Start the RestartService, which initiates the service restart cycle.
62+
// TODO: Request permissions when the API level is high enough
63+
// to require FOREGROUND_SERVICE or POST_NOTIFICATIONS
64+
Intent restartServiceIntent = new Intent(MainActivity.this, RestartService.class);
65+
if (Build.VERSION.SDK_INT >= 26) {
66+
startForegroundService(restartServiceIntent);
67+
} else {
68+
startService(restartServiceIntent);
69+
}
70+
finish();
71+
}
72+
});
4573
}
4674
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.jakewharton.processphoenix.sample;
2+
3+
import android.Manifest;
4+
import android.annotation.TargetApi;
5+
import android.app.Notification;
6+
import android.app.NotificationChannel;
7+
import android.app.NotificationManager;
8+
import android.content.Context;
9+
import android.content.pm.PackageManager;
10+
import android.os.Build;
11+
import android.util.Log;
12+
13+
public final class NotificationBuilder {
14+
15+
/**
16+
* Create a Notification, required to support Service restarting on Android 8 and newer
17+
*/
18+
@TargetApi(26)
19+
public static Notification createNotification(Context context) {
20+
// Android 13 or higher requires a permission to post Notifications
21+
if (Build.VERSION.SDK_INT >= 33) {
22+
if (context.checkCallingOrSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
23+
!= PackageManager.PERMISSION_GRANTED) {
24+
Log.e(
25+
"ProcessPhoenix",
26+
"Required POST_NOTIFICATIONS permission was not granted, cannot restart Service");
27+
return null;
28+
}
29+
}
30+
31+
// Android 8 or higher requires a Notification Channel
32+
if (Build.VERSION.SDK_INT >= 26) {
33+
// Creating an existing notification channel with its original values performs no operation,
34+
// so it's safe to call this code multiple times
35+
NotificationChannel channel =
36+
new NotificationChannel(
37+
"ProcessPhoenix", "ProcessPhoenix", NotificationManager.IMPORTANCE_NONE);
38+
39+
// Create Notification Channel
40+
NotificationManager notificationManager =
41+
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
42+
notificationManager.createNotificationChannel(channel);
43+
}
44+
45+
// Create a Notification
46+
return new Notification.Builder(context, "ProcessPhoenix")
47+
.setSmallIcon(android.R.mipmap.sym_def_app_icon)
48+
.setContentTitle("ProcessPhoenix")
49+
.setContentText("PhoenixService")
50+
.build();
51+
}
52+
}

0 commit comments

Comments
 (0)