Commit f248c032 authored by Sharaf Zaman's avatar Sharaf Zaman
Browse files

Android: Make saving operation on exit more robust

Sometimes if file operation took too long some devices (not all) would
kill our saving thread after a while which would leave saving operation
incomplete. In this change rather than relying on mercy of vendor we
start a Foreground Service which will effectively stop vendor from
killing the process! Once we're done saving we kill the process
overselves.

(cherry picked from commit cff205c2)

# Conflicts:
#	packaging/android/apk/AndroidManifest.xml
parent 6ea4c3a4
<?xml version='1.0' encoding='utf-8'?>
<manifest package="org.krita" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="4.4.5" android:installLocation="auto">
<manifest package="org.krita" xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application android:hardwareAccelerated="true"
android:name="org.qtproject.qt5.android.bindings.QtApplication"
android:label="Krita"
......@@ -10,7 +13,8 @@
android:screenOrientation="unspecified"
android:resizeableActivity="true"
android:theme="@style/DefaultTheme"
android:launchMode="singleTask">
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
......@@ -135,6 +139,8 @@
<activity android:name="org.qtproject.qt5.android.bindings.QtActivity" />
<service android:name=".android.DocumentSaverService" />
</application>
<supports-screens
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</group>
</vector>
......@@ -10,5 +10,6 @@
<string name="remote_action_circleccw">Circle Counter-Clockwise</string>
<string name="something_wrong">Something went wrong...</string>
<string name="cancelled">Cancelled</string>
<string name="save_notification_text">Saving Document</string>
<string name="cancel_save_notification">Cancel</string>
</resources>
/*
* SPDX-FileCopyrightText: 2021 Sharaf Zaman <sharafzaz121@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.krita.android;
import static android.os.Process.killProcess;
import static android.os.Process.myPid;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import org.krita.R;
public class DocumentSaverService extends Service {
private final String TAG = "DocumentSaverService";
private final String CHANNEL_ID = "org.krita.android";
private final int NOTIFICATION_ID = 3;
public static final String START_SAVING = "START_SAVING";
public static final String KILL_PROCESS = "KILL_PROCESS";
private static final String CANCEL_SAVING = "CANCEL_SAVING";
private boolean mKillProcess = false;
private Thread mDocSaverThread;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Creating");
Intent intent = new Intent(this, DocumentSaverService.class);
intent.putExtra(CANCEL_SAVING, true);
PendingIntent cancelPendingIntent = PendingIntent.getService(this, 0,
intent, 0);
createNotificationChannel();
Notification notification;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
notification = new Notification.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.save_notification_text))
.setSmallIcon(R.drawable.ic_save_notification)
.addAction(new Notification.Action.Builder(null,
getString(R.string.cancel_save_notification), cancelPendingIntent).build())
.setProgress(0, 0, true)
.build();
} else {
notification = new Notification.Builder(this)
.setContentTitle(getString(R.string.save_notification_text))
.setSmallIcon(R.drawable.ic_save_notification)
.setPriority(Notification.PRIORITY_LOW)
.setProgress(0, 0, true)
.build();
}
startForeground(NOTIFICATION_ID, notification);
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Default";
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "Starting");
if (intent.getBooleanExtra(START_SAVING, false)) {
Log.d(TAG, "Starting Auto Save");
// let's not block the Android UI thread if we return to the app quickly
mDocSaverThread = new Thread(new Runnable() {
@Override
public void run() {
JNIWrappers.saveState();
stopSelf();
}
});
mDocSaverThread.start();
} else if (intent.getBooleanExtra(KILL_PROCESS, false)) {
mKillProcess = true;
new Thread(new Runnable() {
@Override
public void run() {
waitForSaving();
// if saving finished a long time ago, we will have to kill the app
stopSelf();
}
}).start();
} else if (intent.getBooleanExtra(CANCEL_SAVING, false)) {
// without this Android will think we crashed
stopSelf();
// TODO: Think about atomicity, which Qt doesn't support for Android
killProcess(myPid());
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
Log.d(TAG, "Destroying: Kill Process = " + mKillProcess);
waitForSaving();
if (mKillProcess) {
// We cannot let the process sit around when the activity is
// destroyed. QtActivity depends on lot of statics. We also can't
// exit (libc) using Qt's methods because they crash.
killProcess(myPid());
}
super.onDestroy();
}
private void waitForSaving() {
if (mDocSaverThread != null) {
try {
mDocSaverThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "Saving Interrupted :" + e.getMessage());
}
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
......@@ -19,6 +19,7 @@
package org.krita.android;
import android.os.Build;
import android.util.Log;
import android.os.Bundle;
import android.content.Intent;
......@@ -34,8 +35,7 @@ import android.util.Log;
public class MainActivity extends QtActivity {
private boolean isStartup = true;
private Thread mDocSaverThread;
private String TAG = "MainActivity";
private final String TAG = "MainActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
......@@ -53,6 +53,7 @@ public class MainActivity extends QtActivity {
}
super.onCreate(savedInstanceState);
Log.d("KritaActivity", "LibsLoaded");
new ConfigsManager().handleAssets(this);
DonationHelper.getInstance();
......@@ -90,14 +91,13 @@ public class MainActivity extends QtActivity {
// isn't loaded, it crashes.
if (!isStartup) {
synchronized(this) {
// let's not block the Android UI thread if we return to the app quickly
mDocSaverThread = new Thread(new Runnable() {
@Override
public void run() {
JNIWrappers.saveState();
}
});
mDocSaverThread.start();
Intent intent = new Intent(this, DocumentSaverService.class);
intent.putExtra(DocumentSaverService.START_SAVING, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
}
}
else {
......@@ -107,23 +107,16 @@ public class MainActivity extends QtActivity {
@Override
public void onDestroy() {
synchronized (this) {
if (mDocSaverThread != null) {
try {
mDocSaverThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted :" + e.getMessage());
}
}
}
// Hack or not, I'm not sure. Beyond this, Qt will invoke libc's exit()
// which doesn't kill our global static properly. So, because Qt app *is*
// supposed to terminate now, this should perfectly safe.
android.os.Process.killProcess(android.os.Process.myPid());
Intent intent = new Intent(this, DocumentSaverService.class);
intent.putExtra(DocumentSaverService.KILL_PROCESS, true);
// Docs say: this method will not be called if the activity's hosting process
// is killed. This means, for us that the service has been stopped.
startService(intent);
super.onDestroy();
}
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && getActionBar() != null &&
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment