Our Service is both dangerous and difficult or some aspects of the survival of services in Android
Instead of introducing
In many practical tasks, various background actions are required, whether it is playing music, exchanging data with the server, or simply monitoring user actions in order to steal credit card details from him. Well, if it doesn’t work out, then at least fill it with targeted advertising using the information received. As a long time ago, everyone knows that in Android such things are made out in the form of a service (Service).
Official documentation says that the Android OS stops the service only if there is not enough memory. However, there are other cases. The user can stop the service by himself using the settings / Apps menu provided to him, there he can make a complete stop of the application. But for this he needs to strain and, in general, be aware of his actions and their consequences. Unfortunately, for the destruction of the service, he has other possibilities that he can use unconsciously. In particular, if in our application at least one Activity, visible in history, was previously launched, then the user can literally take out the corresponding task with just a flick of the finger. Paradoxically, along the way, Android will knock out the whole process along with the service.
Personally, this Android behavior does not seem logical to me. The user often simply cleans Recent Apps of the long-forgotten trash, it does not necessarily mean that he wants to abandon the benefits that the running service provided him. However, Google developers thought a little differently. In a different way, in a different way, their right is, but in the end, you and I need to live somehow.
So, the framework of the simplest application for practicing fighting techniques.
SomeActivity.java:
public class SomeActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("Test", "Activity: onCreate");
Intent serviceIntent = new Intent(this, MyService.class);
startService(serviceIntent);
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i("Test", "Activity: onDestroy");
}
}
KamikadzeService.java:
public class KamikadzeService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
Log.i("Test", "Service: onCreate");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("Test", "Service: onStartCommand");
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("Test", "Service: onDestroy");
}
@Override
public void onTaskRemoved(Intent rootIntent) {
Log.i("Test", "Service: onTaskRemoved");
}
}
Everything is elementary here. SomeActivity, when created, starts the KamikadzeService service, which, in turn, starts as sticky or sticky. For agents of hostile platforms, I will explain that the service at startup instructs the operating system to restart it at the earliest opportunity if the service unexpectedly ends. She does this by returning START_STICKY from the onStartCommand method. If the service is not sticky, then after the user deletes the task, there will be no chance of a rebirth after death.
The onTaskRemoved method is called by the system just when a user deletes a task. It is absolutely necessary to mention the attribute of the android: stopWithTask service, which can be set in the manifest. As you can guess by its name (or simply by reading the documentation), if android: stopWithTask = ”true”, then the voluntary movement of the user's finger on the desired square in the Recent Tasks List will stop the service along with deleting the task. Since in this case the service will be considered as agreeable to stop, then no one will restart automatically - she died, she died.
At the very beginning of my struggle for the relative stability of services, having discovered this flag, I was naive to assume that the problem would be solved by setting android: stopWithTask = "false" and the service would no longer die with the task. Alas, reality and dreams had a number of significant differences. Indeed, in this case, the system will not stop the service. She will simply beat her without a warning. By the way, by default this attribute is “false”, from which it was already possible to guess that setting it explicitly will not lead to anything
For equally simple-minded developers, I’ll summarize: the value of the attribute of the android: stopWithTask service does not affect its chances to stay alive after the user has deleted the task, the service is doomed anyway. This attribute only determines which method the service will be called before being destroyed. If it is true, then the onDestroy method will be called on the service (not in all, to put it mildly, cases, but more on that later). And if the attribute is “false”, then the last breath of the service, noticeable to the developer, will be the launch of the onTaskRemoved method.
Having studied all this and experimented with the above program, we can draw the following conclusion: we can’t avoid the death of the background service when the task is deleted. Well, it will not work and it will not work, in the end no one promised an easy life. Since the system can restart our sticky service, let it do it. And we will simply maintain its condition from time to time, restoring it when the service is restored from the ashes. Alas, not so simple.
KitKat. No rest for the wicked
Back in the USSR, in the late 80s, as part of the series “Evenings with Thames Television”, they showed KitKat chocolate ads. Nobody had heard of any KitKat at that time, but the advertisement was new and looked through it with interest. And I perfectly remembered the slogan, which now stuck in the name of the section. For reflects.
As a preface. It was mentioned above that when android: stopWithTask = "true" the service stops exactly, that is, before death it gets its soothing onDestroy. So it was before the advent of Android KitKat, with the advent of which everything has elusively changed. When a user deletes a task in this and later versions of Android, the service will go to another world ... without a trace. In the vast majority of cases. Unless, of course, not counting a possible call to onDestroy in an Activity that falls under the user's finger. Obviously, all this makes android: stopWithTask completely useless for our purposes.
But the release of Android KitKat was well remembered by background service developers for no reason at all. The fact is that in the initial versions of this version there was one interesting detail, which at one time personally drove me into a state of deep depression. KitKat never restarted sticky services.
And then there were screams of programmers' souls on stackoverflow, lots of tickets on Google, corrections, updates, etc. How many of these firmwares are still alive on devices, no one knows. But the fact that they still come across is for sure. Head-on solution with an attempt to restart the service
@Override
public void onTaskRemoved(Intent rootIntent) {
Log.i("Test", "Service: onTaskRemoved");
if (Build.VERSION.SDK_INT == 19)
{
Intent restartIntent = new Intent(this, getClass());
startService(restartIntent);
}
}
It doesn’t give anything, since Android will first work off the start, and only then, with a clear conscience, will destroy the service. Here you have to add a crutch in the form of AlarmManager:
@Override
public void onTaskRemoved(Intent rootIntent) {
Log.i("Test", "Service: onTaskRemoved");
if (Build.VERSION.SDK_INT == 19)
{
Intent restartIntent = new Intent(this, getClass());
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
PendingIntent pi = PendingIntent.getService(this, 1, restartIntent,
PendingIntent.FLAG_ONE_SHOT);
restartIntent.putExtra(“RESTART”
am.setExact(AlarmManager.RTC, System.currentTimeMillis() + 3000, pi);
}
}
That is, we plan to restart the service manually three seconds after deleting the task. Time taken from the ceiling.
Intelligence groped the front line
The one who read this article first remembers my statement about the inevitability of the death of the background service when deleting a task. I admit, I was a little manipulating the terms here. In fact, the service has a way to stay safe and sound, but already in the form of foreground service. For example, like this:
public class KamikadzeService extends Service {
// ...
@Override
public void onCreate() {
Log.i("Test", "Service: onCreate");
Notification.Builder builder = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher);
Notification notification;
if (Build.VERSION.SDK_INT < 16)
notification = builder.getNotification();
else
notification = builder.build();
startForeground(777, notification);
}
// ...
}
When creating a service, it creates a notification, in our case it is just an application icon. The generated notification is passed to the startForeground method and - voila - the service becomes almost immortal. Deleting a task will not affect it in any way, and even if there is not enough memory, it will stop only in the most extreme case. Almost the only way to stop it is to click the appropriate buttons in Settings / Apps, which, in general, was required. So why did I fence the garden before? And the point is in this very notification, which Google has long been demanding to transfer the service to the forefront. It is noticeable to the user, noticeable even if you create it with a transparent icon. And for a number of applications this is not always good. I'm not talking about trojans and other malicious programs, their creators are hardly concerned about the described problem at all, since by definition they should not show the user something for which he can pull. Just showing notifications that are not necessitated by a real need looks, in my opinion, silly. The user also feels this and often even annoys him, as can be seen from the comments on some applications on Google Play.
But we also found methods against notifications, though this is no longer a crutch, but rather a hack. Add another service to the project:
public class HideNotificationService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
Notification.Builder builder = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher);
Notification notification;
if (Build.VERSION.SDK_INT < 16)
notification = builder.getNotification();
else
notification = builder.build();
startForeground(777, notification);
stopForeground(true);
}
}
And we rewrite onCreate in KamikadzeService like this:
public class KamikadzeService extends Service {
// ...
@Override
public void onCreate() {
Log.i("Test", "Service: onCreate");
Notification.Builder builder = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher);
Notification notification;
if (Build.VERSION.SDK_INT < 16)
notification = builder.getNotification();
else
notification = builder.build();
startForeground(777, notification);
Intent hideIntent = new Intent(this, HideNotificationService.class);
startService(hideIntent);
}
// ...
}
The essence of the approach is that the HideNotificationService service, having come out of the foreground with the same identifier 777, goes back to the background with the removal of its notification. At the same time, the KamikadzeService notification is also being destroyed, but the latter remains in the foreground, and it is already "at first glance, as if it were not visible." After that, the HideNotificationService service terminates. It should be clarified that the order of starting services, as well as their entry into the foreground, does not matter here, the main thing is to ensure that the second stopForeground (HideNotificationService) is called later than the first startForeground (KamikadzeService). And the equality of identifiers passed to startForeground is required.
And here again the reasonable question arises - if all this works fine, why did I chew for a long time and tediously about the habits of "purely" background services? Yes, because the described technique is a hack and a hack dirty enough to be used for a long time. Although in the emulator with the arrival of the other day Android 6.0 it still works. To hope or not to hope is for the reader to decide.