Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,19 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter android:label="Pocket Paint">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
95 changes: 70 additions & 25 deletions android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package org.catrobat.paintroid

import android.Manifest
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.NonNull
Expand All @@ -15,6 +17,7 @@ import io.flutter.plugin.common.MethodChannel
import java.io.IOException

class MainActivity : FlutterActivity() {
private var initialFileUri: String? = null
private val hasWritePermission: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
ContextCompat.checkSelfPermission(
Expand All @@ -26,22 +29,64 @@ class MainActivity : FlutterActivity() {
super.configureFlutterEngine(flutterEngine)
setupPhotoLibraryChannel(flutterEngine)
setupDeviceChannel(flutterEngine)
setupFileHandlerChannel(flutterEngine)
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}

private fun handleIntent(intent: Intent?) {
val action = intent?.action
val data: Uri? = intent?.data
if ((Intent.ACTION_VIEW == action || Intent.ACTION_EDIT == action) && data != null) {
initialFileUri = data.toString()
}
}

private fun setupFileHandlerChannel(flutterEngine: FlutterEngine) {
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/file_handler"
).setMethodCallHandler { call, result ->
when (call.method) {
"getInitialFile" -> {
result.success(initialFileUri)
initialFileUri = null
}
"getFileBytes" -> {
val uriString = call.argument<String>("uri")
if (uriString != null) {
try {
val uri = Uri.parse(uriString)
val bytes = contentResolver.openInputStream(uri)?.use { it.readBytes() }
result.success(bytes)
} catch (e: Exception) {
result.error("IO_ERROR", "Failed to read URI: ${e.message}", null)
}
} else {
result.error("INVALID_ARGUMENT", "URI is null", null)
}
}
else -> result.notImplemented()
}
}
}

private fun setupDeviceChannel(flutterEngine: FlutterEngine) {
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/device"
).apply {
setMethodCallHandler { call, result ->
when (call.method) {
"getHeightInPixels" -> {
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeMaximumWindowMetrics(activity)
val height = windowMetrics.bounds.height()
result.success(height.toDouble())
}
else -> result.notImplemented()
setMethodCallHandler { call, result ->
when (call.method) {
"getHeightInPixels" -> {
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeMaximumWindowMetrics(activity)
val height = windowMetrics.bounds.height()
result.success(height.toDouble())
}
else -> result.notImplemented()
}
}
}
}
Expand All @@ -50,24 +95,24 @@ class MainActivity : FlutterActivity() {
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/photo_library"
).apply {
setMethodCallHandler { call, result ->
when (call.method) {
"saveToPhotos" -> {
if (!hasWritePermission) {
result.error(
"PERMISSION_DENIED",
"User explicitly denied WRITE_EXTERNAL_STORAGE permission",
null
)
return@setMethodCallHandler
}
val (filename, imageData) = extractImageData(call, result)
?: return@setMethodCallHandler
setMethodCallHandler { call, result ->
when (call.method) {
"saveToPhotos" -> {
if (!hasWritePermission) {
result.error(
"PERMISSION_DENIED",
"User explicitly denied WRITE_EXTERNAL_STORAGE permission",
null
)
return@setMethodCallHandler
}
val (filename, imageData) = extractImageData(call, result)
?: return@setMethodCallHandler
saveImageToPictures(filename, imageData)
result.success(null)
}
else -> result.notImplemented()
}
else -> result.notImplemented()
}
}
}
}
Expand Down Expand Up @@ -99,7 +144,7 @@ class MainActivity : FlutterActivity() {
return null
}
val imageData = call.argument<ByteArray>("data") ?: run {
result.error(
result.error(
"INVALID_IMAGE_DATA",
"Image data is either not supplied or not of type UInt8List",
null
Expand Down
7 changes: 4 additions & 3 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import 'package:paintroid/ui/theme/theme.dart';

class App extends StatelessWidget {
final bool showOnboardingPage;
final String? initialFileUri;

App({super.key, required this.showOnboardingPage});
App({super.key, required this.showOnboardingPage,this.initialFileUri});

final _lightTheme = LightPaintroidThemeData();
final _darkTheme = DarkPaintroidThemeData();
Expand Down Expand Up @@ -42,7 +43,7 @@ class App extends StatelessWidget {
? const OnboardingPage(
navigateTo: LandingPage(title: 'Pocket Paint'),
)
: const LandingPage(title: 'Pocket Paint'),
:LandingPage(title: 'Pocket Paint',initialFileUri:initialFileUri),
);
case '/PocketPaint':
return MaterialPageRoute(
Expand All @@ -66,7 +67,7 @@ class App extends StatelessWidget {
child: child,
);
},
child: const LandingPage(title: 'Pocket Paint'),
child: LandingPage(title: 'Pocket Paint',initialFileUri:initialFileUri),
),
),
);
Expand Down
13 changes: 11 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:developer';

import 'package:flutter/material.dart';

import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
Expand All @@ -25,8 +25,17 @@ void main() async {
);

WidgetsFlutterBinding.ensureInitialized();
const platform = MethodChannel('org.catrobat.paintroid/file_handler');
String? initialFileUri;

try {
initialFileUri = await platform.invokeMethod('getInitialFile');
} on PlatformException catch (e) {
log("Failed to get initial file: '${e.message}'.");
}

final prefs = await SharedPreferences.getInstance();
final showOnboarding = prefs.getBool('showOnboarding') ?? true;

runApp(ProviderScope(child: App(showOnboardingPage: showOnboarding)));
runApp(ProviderScope(child: App(showOnboardingPage: showOnboarding,initialFileUri: initialFileUri)));
}
62 changes: 59 additions & 3 deletions lib/ui/pages/landing_page/landing_page.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'dart:developer';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:oxidized/oxidized.dart';
import 'package:paintroid/core/database/project_database.dart';
Expand All @@ -23,8 +26,9 @@ import 'package:toast/toast.dart';

class LandingPage extends ConsumerStatefulWidget {
final String title;

const LandingPage({super.key, required this.title});
final String? initialFileUri;

const LandingPage({super.key, required this.title,this.initialFileUri});

@override
ConsumerState<LandingPage> createState() => _LandingPageState();
Expand All @@ -35,13 +39,65 @@ class _LandingPageState extends ConsumerState<LandingPage> {
late IFileService fileService;
late IImageService imageService;

@override
void initState() {
super.initState();

final platform = MethodChannel('org.catrobat.paintroid/file_handler');
SystemChannels.lifecycle.setMessageHandler((msg) async {
if (msg == AppLifecycleState.resumed.toString()) {
final String? newUri = await platform.invokeMethod('getInitialFile');
if (newUri != null) {
_handleInitialFile(newUri);
}
}
return null;
});

if (widget.initialFileUri != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleInitialFile(widget.initialFileUri!);
});
}
}

Future<void> _handleInitialFile(String uri) async {
if (mounted) {
final ioHandler = ref.read(IOHandler.provider);
final shouldContinue = await ioHandler.handleUnsavedChanges(context, this);
if (!shouldContinue) return;
}

ref.read(workspaceStateProvider.notifier).performIOTask(() async {
try {
final platform = const MethodChannel('org.catrobat.paintroid/file_handler');
final Uint8List? imageBytes = await platform.invokeMethod('getFileBytes', {'uri': uri});

if (imageBytes != null && mounted) {
_clearCanvas();
final ui.Image image = await decodeImageFromList(imageBytes);
ref.read(canvasStateProvider.notifier).setBackgroundImage(image);
if (!mounted) return;
final currentRoute = ModalRoute.of(context)?.settings.name;
if (currentRoute != '/PocketPaint') {
await _navigateToPocketPaint();
} else {
setState(() {});
}
}
} catch (e) {
log('error in loading file from Intent: $e');
ToastUtils.showShortToast(message: 'failed to open image from file.');
}
});
}
Future<List<Project>> _getProjects() async {
return database.projectDAO.getProjects();
}

Future<void> _navigateToPocketPaint() async {
await Navigator.pushNamed(context, '/PocketPaint');
setState(() {});
if (mounted){setState(() {});}
}

Future<bool> _loadProject(IOHandler ioHandler, Project project) async {
Expand Down
Loading