Skip to content

Commit 1ac939a

Browse files
fix(android): Second instance of app is opened when catrobat image is opened from files [PAINTROID-776]
1 parent 9f41f43 commit 1ac939a

5 files changed

Lines changed: 142 additions & 34 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,19 @@
2929
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
3030
android:exported="true"
3131
android:hardwareAccelerated="true"
32-
android:launchMode="singleTop"
32+
android:launchMode="singleTask"
3333
android:theme="@style/LaunchTheme"
3434
android:windowSoftInputMode="adjustResize">
3535
<meta-data
3636
android:name="io.flutter.embedding.android.NormalTheme"
3737
android:resource="@style/NormalTheme" />
38+
<intent-filter android:label="Pocket Paint">
39+
<action android:name="android.intent.action.VIEW" />
40+
<action android:name="android.intent.action.EDIT" />
41+
<category android:name="android.intent.category.DEFAULT" />
42+
<category android:name="android.intent.category.BROWSABLE" />
43+
<data android:mimeType="image/*" />
44+
</intent-filter>
3845
<intent-filter>
3946
<action android:name="android.intent.action.MAIN" />
4047
<category android:name="android.intent.category.LAUNCHER" />

android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package org.catrobat.paintroid
22

33
import android.Manifest
44
import android.content.ContentValues
5+
import android.content.Intent
56
import android.content.pm.PackageManager
7+
import android.net.Uri
68
import android.os.Build
79
import android.provider.MediaStore
810
import androidx.annotation.NonNull
@@ -15,6 +17,7 @@ import io.flutter.plugin.common.MethodChannel
1517
import java.io.IOException
1618

1719
class MainActivity : FlutterActivity() {
20+
private var initialFileUri: String? = null
1821
private val hasWritePermission: Boolean
1922
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
2023
ContextCompat.checkSelfPermission(
@@ -26,22 +29,64 @@ class MainActivity : FlutterActivity() {
2629
super.configureFlutterEngine(flutterEngine)
2730
setupPhotoLibraryChannel(flutterEngine)
2831
setupDeviceChannel(flutterEngine)
32+
setupFileHandlerChannel(flutterEngine)
33+
handleIntent(intent)
34+
}
35+
override fun onNewIntent(intent: Intent) {
36+
super.onNewIntent(intent)
37+
handleIntent(intent)
38+
}
39+
40+
private fun handleIntent(intent: Intent?) {
41+
val action = intent?.action
42+
val data: Uri? = intent?.data
43+
if ((Intent.ACTION_VIEW == action || Intent.ACTION_EDIT == action) && data != null) {
44+
initialFileUri = data.toString()
45+
}
46+
}
47+
48+
private fun setupFileHandlerChannel(flutterEngine: FlutterEngine) {
49+
MethodChannel(
50+
flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/file_handler"
51+
).setMethodCallHandler { call, result ->
52+
when (call.method) {
53+
"getInitialFile" -> {
54+
result.success(initialFileUri)
55+
initialFileUri = null
56+
}
57+
"getFileBytes" -> {
58+
val uriString = call.argument<String>("uri")
59+
if (uriString != null) {
60+
try {
61+
val uri = Uri.parse(uriString)
62+
val bytes = contentResolver.openInputStream(uri)?.use { it.readBytes() }
63+
result.success(bytes)
64+
} catch (e: Exception) {
65+
result.error("IO_ERROR", "Failed to read URI: ${e.message}", null)
66+
}
67+
} else {
68+
result.error("INVALID_ARGUMENT", "URI is null", null)
69+
}
70+
}
71+
else -> result.notImplemented()
72+
}
73+
}
2974
}
3075

3176
private fun setupDeviceChannel(flutterEngine: FlutterEngine) {
3277
MethodChannel(
3378
flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/device"
3479
).apply {
35-
setMethodCallHandler { call, result ->
36-
when (call.method) {
37-
"getHeightInPixels" -> {
38-
val windowMetrics = WindowMetricsCalculator.getOrCreate()
39-
.computeMaximumWindowMetrics(activity)
40-
val height = windowMetrics.bounds.height()
41-
result.success(height.toDouble())
42-
}
43-
else -> result.notImplemented()
80+
setMethodCallHandler { call, result ->
81+
when (call.method) {
82+
"getHeightInPixels" -> {
83+
val windowMetrics = WindowMetricsCalculator.getOrCreate()
84+
.computeMaximumWindowMetrics(activity)
85+
val height = windowMetrics.bounds.height()
86+
result.success(height.toDouble())
4487
}
88+
else -> result.notImplemented()
89+
}
4590
}
4691
}
4792
}
@@ -50,24 +95,24 @@ class MainActivity : FlutterActivity() {
5095
MethodChannel(
5196
flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/photo_library"
5297
).apply {
53-
setMethodCallHandler { call, result ->
54-
when (call.method) {
55-
"saveToPhotos" -> {
56-
if (!hasWritePermission) {
57-
result.error(
58-
"PERMISSION_DENIED",
59-
"User explicitly denied WRITE_EXTERNAL_STORAGE permission",
60-
null
61-
)
62-
return@setMethodCallHandler
63-
}
64-
val (filename, imageData) = extractImageData(call, result)
65-
?: return@setMethodCallHandler
98+
setMethodCallHandler { call, result ->
99+
when (call.method) {
100+
"saveToPhotos" -> {
101+
if (!hasWritePermission) {
102+
result.error(
103+
"PERMISSION_DENIED",
104+
"User explicitly denied WRITE_EXTERNAL_STORAGE permission",
105+
null
106+
)
107+
return@setMethodCallHandler
108+
}
109+
val (filename, imageData) = extractImageData(call, result)
110+
?: return@setMethodCallHandler
66111
saveImageToPictures(filename, imageData)
67112
result.success(null)
68-
}
69-
else -> result.notImplemented()
70113
}
114+
else -> result.notImplemented()
115+
}
71116
}
72117
}
73118
}
@@ -99,7 +144,7 @@ class MainActivity : FlutterActivity() {
99144
return null
100145
}
101146
val imageData = call.argument<ByteArray>("data") ?: run {
102-
result.error(
147+
result.error(
103148
"INVALID_IMAGE_DATA",
104149
"Image data is either not supplied or not of type UInt8List",
105150
null

lib/app.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import 'package:paintroid/ui/theme/theme.dart';
1111

1212
class App extends StatelessWidget {
1313
final bool showOnboardingPage;
14+
final String? initialFileUri;
1415

15-
App({super.key, required this.showOnboardingPage});
16+
App({super.key, required this.showOnboardingPage,this.initialFileUri});
1617

1718
final _lightTheme = LightPaintroidThemeData();
1819
final _darkTheme = DarkPaintroidThemeData();
@@ -42,7 +43,7 @@ class App extends StatelessWidget {
4243
? const OnboardingPage(
4344
navigateTo: LandingPage(title: 'Pocket Paint'),
4445
)
45-
: const LandingPage(title: 'Pocket Paint'),
46+
:LandingPage(title: 'Pocket Paint',initialFileUri:initialFileUri),
4647
);
4748
case '/PocketPaint':
4849
return MaterialPageRoute(
@@ -66,7 +67,7 @@ class App extends StatelessWidget {
6667
child: child,
6768
);
6869
},
69-
child: const LandingPage(title: 'Pocket Paint'),
70+
child: LandingPage(title: 'Pocket Paint',initialFileUri:initialFileUri),
7071
),
7172
),
7273
);

lib/main.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:developer';
22

33
import 'package:flutter/material.dart';
4-
4+
import 'package:flutter/services.dart';
55
import 'package:flutter_riverpod/flutter_riverpod.dart';
66
import 'package:logging/logging.dart';
77
import 'package:shared_preferences/shared_preferences.dart';
@@ -25,8 +25,17 @@ void main() async {
2525
);
2626

2727
WidgetsFlutterBinding.ensureInitialized();
28+
const platform = MethodChannel('org.catrobat.paintroid/file_handler');
29+
String? initialFileUri;
30+
31+
try {
32+
initialFileUri = await platform.invokeMethod('getInitialFile');
33+
} on PlatformException catch (e) {
34+
log("Failed to get initial file: '${e.message}'.");
35+
}
36+
2837
final prefs = await SharedPreferences.getInstance();
2938
final showOnboarding = prefs.getBool('showOnboarding') ?? true;
3039

31-
runApp(ProviderScope(child: App(showOnboardingPage: showOnboarding)));
40+
runApp(ProviderScope(child: App(showOnboardingPage: showOnboarding,initialFileUri: initialFileUri)));
3241
}

lib/ui/pages/landing_page/landing_page.dart

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import 'dart:developer';
2+
import 'dart:typed_data';
3+
import 'dart:ui' as ui;
14
import 'package:flutter/material.dart';
5+
import 'package:flutter/services.dart';
26
import 'package:flutter_riverpod/flutter_riverpod.dart';
37
import 'package:oxidized/oxidized.dart';
48
import 'package:paintroid/core/database/project_database.dart';
@@ -23,8 +27,9 @@ import 'package:toast/toast.dart';
2327

2428
class LandingPage extends ConsumerStatefulWidget {
2529
final String title;
26-
27-
const LandingPage({super.key, required this.title});
30+
final String? initialFileUri;
31+
32+
const LandingPage({super.key, required this.title,this.initialFileUri});
2833

2934
@override
3035
ConsumerState<LandingPage> createState() => _LandingPageState();
@@ -35,13 +40,54 @@ class _LandingPageState extends ConsumerState<LandingPage> {
3540
late IFileService fileService;
3641
late IImageService imageService;
3742

43+
@override
44+
void initState() {
45+
super.initState();
46+
47+
final platform = MethodChannel('org.catrobat.paintroid/file_handler');
48+
SystemChannels.lifecycle.setMessageHandler((msg) async {
49+
if (msg == AppLifecycleState.resumed.toString()) {
50+
final String? newUri = await platform.invokeMethod('getInitialFile');
51+
if (newUri != null) {
52+
_handleInitialFile(newUri);
53+
}
54+
}
55+
return null;
56+
});
57+
58+
if (widget.initialFileUri != null) {
59+
WidgetsBinding.instance.addPostFrameCallback((_) {
60+
_handleInitialFile(widget.initialFileUri!);
61+
});
62+
}
63+
}
64+
65+
Future<void> _handleInitialFile(String uri) async {
66+
ref.read(workspaceStateProvider.notifier).performIOTask(() async {
67+
try {
68+
final platform = MethodChannel('org.catrobat.paintroid/file_handler');
69+
final Uint8List? imageBytes = await platform.invokeMethod('getFileBytes', {'uri': uri});
70+
71+
if (imageBytes != null && mounted) {
72+
_clearCanvas();
73+
final ui.Image image = await decodeImageFromList(imageBytes);
74+
ref.read(canvasStateProvider.notifier).setBackgroundImage(image);
75+
await _navigateToPocketPaint();
76+
}
77+
} catch (e) {
78+
log("error in loading file from Intent: $e");
79+
ToastUtils.showShortToast(message: "failed to open image from file.");
80+
}
81+
});
82+
}
83+
3884
Future<List<Project>> _getProjects() async {
3985
return database.projectDAO.getProjects();
4086
}
4187

4288
Future<void> _navigateToPocketPaint() async {
4389
await Navigator.pushNamed(context, '/PocketPaint');
44-
setState(() {});
90+
if (mounted){setState(() {});}
4591
}
4692

4793
Future<bool> _loadProject(IOHandler ioHandler, Project project) async {

0 commit comments

Comments
 (0)