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
15 changes: 15 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<!-- Screen Saver (Dream) Service -->
<service
android:name=".PhotoFrameDreamService"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/dream_label"
android:permission="android.permission.BIND_DREAM_SERVICE">
<intent-filter>
<action android:name="android.service.dreams.DreamService" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.service.dream"
android:resource="@xml/dream_info" />
</service>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.micw.openphotoframe

import android.content.Intent
import android.os.Bundle
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
Expand All @@ -15,16 +16,29 @@ class MainActivity : FlutterActivity() {
private lateinit var screenControlHandler: ScreenControlHandler
private lateinit var keepAliveHandler: KeepAliveHandler
private lateinit var updaterHandler: UpdaterHandler
var isDreamMode = false
private set

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

isDreamMode = intent?.getBooleanExtra("is_dream", false) ?: false
Log.d("MainActivity", "isDreamMode: $isDreamMode")

// Debug logging for screen size detection
val display = windowManager.defaultDisplay
val size = android.graphics.Point()
display.getSize(size)
Log.d("MainActivity", "Window size onCreate: ${size.x}x${size.y}")
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
isDreamMode = intent.getBooleanExtra("is_dream", false)
Log.d("MainActivity", "onNewIntent isDreamMode: $isDreamMode")
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.micw.openphotoframe

import android.content.Intent
import android.service.dreams.DreamService

class PhotoFrameDreamService : DreamService() {
override fun onAttachedToWindow() {
super.onAttachedToWindow()

isInteractive = false
isFullscreen = true

val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra("is_dream", true)
}
startActivity(intent)
finish()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ class ScreenControlHandler(private val context: Context) {
"isScreenOn" -> {
result.success(powerManager.isInteractive)
}
"isDreamMode" -> {
val activity = context as? MainActivity
result.success(activity?.isDreamMode ?: false)
}
"exitApp" -> {
val activity = context as? android.app.Activity
if (activity != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.finishAndRemoveTask()
} else {
activity.finish()
}
result.success(true)
} else {
result.error("UNAVAILABLE", "Activity not available", null)
}
}
else -> {
result.notImplemented()
}
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="dream_label">Open Photo Frame</string>
</resources>
2 changes: 2 additions & 0 deletions android/app/src/main/res/xml/dream_info.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<dream xmlns:android="http://schemas.android.com/apk/res/android" />
30 changes: 30 additions & 0 deletions lib/infrastructure/services/native_screen_control_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
/// - Turn the screen completely off using lockNow()
/// - Schedule wake-up using AlarmManager
/// - Wake the screen immediately
/// - Handle Dream Mode (Daydream) interactions
///
/// Requires Device Admin permission to be enabled by the user.
class NativeScreenControlService {
Expand Down Expand Up @@ -125,4 +126,33 @@ class NativeScreenControlService {
return true;
}
}

/// Check if the app is currently running in Dream Mode (Daydream/Screen Saver).
static Future<bool> isDreamMode() async {
if (!isSupported) return false;

try {
final result = await _channel.invokeMethod<bool>('isDreamMode');
return result ?? false;
} catch (e) {
print('Error checking Dream Mode: $e');
return false;
}
}

/// Exit the app and remove it from recent tasks.
/// This is used to return to the previous state (e.g., from Dream Mode).
static Future<void> exitApp() async {
if (!isSupported) {
// For non-Android platforms, we just pop the current route
// which might not exit the app but is the closest behavior.
return;
}

try {
await _channel.invokeMethod('exitApp');
} catch (e) {
print('Error exiting app: $e');
}
}
}
1 change: 1 addition & 0 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@

"noPhotosFound": "Keine Fotos gefunden",
"tapCenterToOpenSettings": "Tippe auf die Bildschirmmitte um Einstellungen zu öffnen",
"tapToExit": "Tippen zum Beenden",

"screenOrientation": "Bildschirmausrichtung",
"screenOrientationAuto": "Automatisch (Sensor)",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@

"noPhotosFound": "No photos found",
"tapCenterToOpenSettings": "Tap center of screen to open settings",
"tapToExit": "Tap to exit",

"screenOrientation": "Screen Orientation",
"screenOrientationAuto": "Automatic (Sensor)",
Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,12 @@ abstract class AppLocalizations {
/// **'Tap center of screen to open settings'**
String get tapCenterToOpenSettings;

/// No description provided for @tapToExit.
///
/// In en, this message translates to:
/// **'Tap to exit'**
String get tapToExit;

/// No description provided for @screenOrientation.
///
/// In en, this message translates to:
Expand Down
3 changes: 3 additions & 0 deletions lib/l10n/app_localizations_de.dart
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ class AppLocalizationsDe extends AppLocalizations {
String get tapCenterToOpenSettings =>
'Tippe auf die Bildschirmmitte um Einstellungen zu öffnen';

@override
String get tapToExit => 'Tippen zum Beenden';

@override
String get screenOrientation => 'Bildschirmausrichtung';

Expand Down
3 changes: 3 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get tapCenterToOpenSettings => 'Tap center of screen to open settings';

@override
String get tapToExit => 'Tap to exit';

@override
String get screenOrientation => 'Screen Orientation';

Expand Down
28 changes: 24 additions & 4 deletions lib/ui/screens/slideshow_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class _SlideshowScreenState extends State<SlideshowScreen> with TickerProviderSt
// Current photo location name (from geocoding)
String? _currentLocationName;

// Dream Mode status
bool _isDreamMode = false;

@override
void initState() {
super.initState();
Expand All @@ -98,6 +101,7 @@ class _SlideshowScreenState extends State<SlideshowScreen> with TickerProviderSt
// Initialize Service
WidgetsBinding.instance.addPostFrameCallback((_) {
_initService();
_checkDreamMode();
_initKeepAliveService();
_showStartupConfigNoticeIfNeeded();
// Schedule init is now handled reactively in build() via _updateDisplaySchedule()
Expand Down Expand Up @@ -157,6 +161,7 @@ class _SlideshowScreenState extends State<SlideshowScreen> with TickerProviderSt
}

// Resume slideshow if it was paused
_checkDreamMode();
_resumeSlideshow();
break;
case AppLifecycleState.detached:
Expand Down Expand Up @@ -436,10 +441,24 @@ class _SlideshowScreenState extends State<SlideshowScreen> with TickerProviderSt
_startTimer();
}

Future<void> _checkDreamMode() async {
final isDream = await NativeScreenControlService.isDreamMode();
if (mounted) {
setState(() => _isDreamMode = isDream);
if (isDream) {
// If we are in dream mode, ensure we are showing the slideshow (root route)
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
}

void _openSettings() {
_timer?.cancel(); // Stop auto-advance while in settings

Navigator.of(context).push(
if (_isDreamMode) {
NativeScreenControlService.exitApp();
} else {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SettingsScreen()),
).then((_) {
// Restore immersive mode after returning from settings
Expand All @@ -452,6 +471,7 @@ class _SlideshowScreenState extends State<SlideshowScreen> with TickerProviderSt
// Restart timer when returning from settings
_startTimer();
});
}
}

Future<void> _transitionTo(PhotoEntry photo, {SlideDirection? slideDirection}) async {
Expand Down Expand Up @@ -673,10 +693,10 @@ class _SlideshowScreenState extends State<SlideshowScreen> with TickerProviderSt
AppLocalizations.of(context)!.noPhotosFound,
style: TextStyle(color: Colors.white, fontSize: 20),
),
SizedBox(height: 8),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.tapCenterToOpenSettings,
style: TextStyle(color: Colors.white54, fontSize: 14),
_isDreamMode ? AppLocalizations.of(context)!.tapToExit : AppLocalizations.of(context)!.tapCenterToOpenSettings,
style: const TextStyle(color: Colors.white54, fontSize: 14),
),
],
),
Expand Down