Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a homescreen widget #144

Open
wants to merge 17 commits into
base: master
Choose a base branch
from

Conversation

Abestanis
Copy link
Collaborator

@Abestanis Abestanis commented Jul 14, 2024

Depends on #130.

Adds a adaptable homescreen widget. Closes #111.

Pictures

Homescreen widget
Homescreen widget more sizes

@Abestanis Abestanis force-pushed the feature/homescreen_widget branch 2 times, most recently from 8301183 to d9b576d Compare October 14, 2024 09:38
@Abestanis Abestanis marked this pull request as ready for review October 14, 2024 09:38
@nt4f04uNd
Copy link
Owner

First impression - this works crazy awesome!

@nt4f04uNd
Copy link
Owner

image

Can we give this bar some bottom padding? Around 8 dp

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, how did you generate this preview? Can we document this process somehow somewhere?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we need to update the preview image after you added padding

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, how did you generate this preview? Can we document this process somehow somewhere?

Android emulators with an API <= 26 have a Widget Preview app that can generate the preview. I added a a comment in the preview XML layout with a reminder and instructions on how to update it.

lib/logic/app_widget.dart Outdated Show resolved Hide resolved
pubspec.yaml Outdated Show resolved Hide resolved
test/routes/home_route_test.dart Outdated Show resolved Hide resolved
@@ -133,6 +135,11 @@ void main() {

testWidgets('home screen - shows when permissions are granted and not searching for tracks',
(WidgetTester tester) async {
late AppWidgetChannelObserver appWidgetChannelObserver;
await tester.runAsync(() => tester.pump()); // Wait for widget events from old app start to process.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old app?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a lot of problems getting these tests to work reliably. This is a workaround because due to our singleton architecture and because tests can end before all futures complete, we were getting events from previous tests on the appWidgetChannelObserver.

old app start is poorly worded, other tests might be better, I'll rephrase it.

@@ -141,6 +148,9 @@ void main() {
tester.getRect(find.byType(App)).height,
reason: 'Player route must be offscreen',
);
await tester.runAsync(() => tester.pump()); // Wait for widget events from start to process.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is even more confusing than the one above

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reworded it. This is another workaround to let some of the callback on streams called in ContentControl.init fire, so the widget events reach the app method channel.

I think it's this chain that needs to complete: ContentControl.init -> MusicPlayer.instance.init -> restoreLastSong -> setSong -> PlaybackControl.instance.changeSong -> PlaybackControl.onSongChange.add -> triggers AppWidgetControl.update.

@nt4f04uNd
Copy link
Owner

As a side note - we probably would like to show title and artist in the widget, but I'm in favor of doing this in a separate PR

<resources>
<dimen name="musicPlayerWidgetButtonSize">48dp</dimen>
<dimen name="appWidgetRadius">16dp</dimen>
<dimen name="appWidgetInnerRadius">8dp</dimen>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do they differ?

Copy link
Owner

@nt4f04uNd nt4f04uNd Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also help me understand what do these radiuses practically mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the rounded corner implementation, see the docs.
The appWidgetRadius refers to the corner radius of the background element of the widget (in our case it's the album art), and the appWidgetInnerRadius is the corner radius of any foreground element in the widget (in our case it's the button bar).

Prior to Android 31 there was no guideline about these, but on 31 and up they are given by the launcher via @android:dimen/system_app_widget_background_radius and @android:dimen/system_app_widget_inner_radius, and the description of system_app_widget_inner_radius says:

This is exactly 8 dp less than the background radius, to align nicely when using an 8 dp padding.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed appWidgetRadius to appWidgetBackgroundRadius which hopefully makes it a bit clearer.

@nt4f04uNd
Copy link
Owner

While I was testing the buld from this PR, one time after changing a song and then immediately minimizing the app to the home page, where there was a widget visible, I got the "The app is not responding notification" from the system. It was also the default art.

I didn't get it anymore since, but maybe it's something to dig into. Maybe it was reported to firebase, I didn't check

@nt4f04uNd
Copy link
Owner

nt4f04uNd commented Oct 20, 2024

I suppose that's the error I got

  Fatal Exception: io.flutter.plugins.firebase.crashlytics.FlutterError: PlatformException(error, Service.startForeground() not allowed due to mAllowStartForeground false: service com.nt4f04und.sweyer/com.ryanheise.audioservice.AudioService, null, android.app.ForegroundServiceStartNotAllowedException: Service.startForeground() not allowed due to mAllowStartForeground false: service com.nt4f04und.sweyer/com.ryanheise.audioservice.AudioService
	at android.app.ForegroundServiceStartNotAllowedException$1.createFromParcel(ForegroundServiceStartNotAllowedException.java:54)
	at android.app.ForegroundServiceStartNotAllowedException$1.createFromParcel(ForegroundServiceStartNotAllowedException.java:50)
	at android.os.Parcel.readParcelableInternal(Parcel.java:4804)
	at android.os.Parcel.readParcelable(Parcel.java:4772)
	at android.os.Parcel.createExceptionOrNull(Parcel.java:3035)
	at android.os.Parcel.createException(Parcel.java:3024)
	at android.os.Parcel.readException(Parcel.java:3007)
	at android.os.Parcel.readException(Parcel.java:2949)
	at android.app.IActivityManager$Stub$Proxy.setServiceForeground(IActivityManager.java:6079)
	at android.app.Service.startForeground(Service.java:797)
	at com.ryanheise.audioservice.AudioService.Q(Unknown Source:21)
	at com.ryanheise.audioservice.AudioService.F(Unknown Source:34)
	at com.ryanheise.audioservice.AudioService.Z(Unknown Source:159)
	at com.ryanheise.audioservice.a$d.E(Unknown Source:481)
	at i6.k$a.a(Unknown Source:17)
	at w5.c.l(Unknown Source:18)
	at w5.c.m(Unknown Source:40)
	at w5.c.i(Unknown Source:0)
	at w5.b.run(Unknown Source:12)
	at android.os.Handler.handleCallback(Handler.java:942)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loopOnce(Looper.java:240)
	at android.os.Looper.loop(Looper.java:351)
	at android.app.ActivityThread.main(ActivityThread.java:8370)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:568)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)
)
#00 pc 0x64c647 com.nt4f04und.sweyer (StandardMethodCodec.decodeEnvelope [message_codecs.dart:653]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#01 pc 0x6a2c23 com.nt4f04und.sweyer (MethodChannel._invokeMethod [platform_channel.dart:315]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#02 pc 0x6909f7 com.nt4f04und.sweyer (_SuspendState._createAsyncStarCallback.<anonymous closure> [async_patch.dart:353]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#03 pc 0x1bff8b com.nt4f04und.sweyer (_FutureListener.handleValue [zone.dart:1660]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#04 pc 0x696353 com.nt4f04und.sweyer (Future._propagateToListeners.handleValueCallback [future_impl.dart:767]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#05 pc 0x1bed8f com.nt4f04und.sweyer (Future._propagateToListeners [future_impl.dart:796]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#06 pc 0x1c3dfb com.nt4f04und.sweyer (Future._completeWithValue [future_impl.dart:567]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#07 pc 0x1c098f com.nt4f04und.sweyer (Future._asyncCompleteWithValue.<anonymous closure> [future_impl.dart:640]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#08 pc 0x1be45f com.nt4f04und.sweyer (_microtaskLoop [schedule_microtask.dart:40]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#09 pc 0x6953c7 com.nt4f04und.sweyer (_startMicrotaskLoop [schedule_microtask.dart:49]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
#10 pc 0x1be3d3 com.nt4f04und.sweyer (_startMicrotaskLoop [schedule_microtask.dart:44]) (BuildId: bca64abf2e8413c431bb8f1cc572ed9d)
        

https://console.firebase.google.com/u/0/project/sweyer-def42/crashlytics/app/android:com.nt4f04und.sweyer/issues/46654a54d48703098a19382a34e51e44?time=last-ninety-days&types=crash&sessionEventKey=6714C7E6005600013DE77BBFCAB31106_2006310376430476968

It's almost an exact duplicate of the other one that we have

https://console.firebase.google.com/u/0/project/sweyer-def42/crashlytics/app/android:com.nt4f04und.sweyer/issues/74dfca6f536b0c14f4e05937e3aaf53f?time=last-ninety-days&types=crash

          Fatal Exception: io.flutter.plugins.firebase.crashlytics.FlutterError: PlatformException(error, Service.startForeground() not allowed due to mAllowStartForeground false: service com.nt4f04und.sweyer/com.ryanheise.audioservice.AudioService, null, android.app.ForegroundServiceStartNotAllowedException: Service.startForeground() not allowed due to mAllowStartForeground false: service com.nt4f04und.sweyer/com.ryanheise.audioservice.AudioService
	at android.app.ForegroundServiceStartNotAllowedException$1.createFromParcel(ForegroundServiceStartNotAllowedException.java:54)
	at android.app.ForegroundServiceStartNotAllowedException$1.createFromParcel(ForegroundServiceStartNotAllowedException.java:50)
	at android.os.Parcel.readParcelableInternal(Parcel.java:5016)
	at android.os.Parcel.readParcelable(Parcel.java:4998)
	at android.os.Parcel.createExceptionOrNull(Parcel.java:3178)
	at android.os.Parcel.createException(Parcel.java:3167)
	at android.os.Parcel.readException(Parcel.java:3150)
	at android.os.Parcel.readException(Parcel.java:3092)
	at android.app.IActivityManager$Stub$Proxy.setServiceForeground(IActivityManager.java:6960)
	at android.app.Service.startForeground(Service.java:863)
	at com.ryanheise.audioservice.AudioService.internalStartForeground(AudioService.java:684)
	at com.ryanheise.audioservice.AudioService.enterPlayingState(AudioService.java:665)
	at com.ryanheise.audioservice.AudioService.setState(AudioService.java:529)
	at com.ryanheise.audioservice.AudioServicePlugin$AudioHandlerInterface.onMethodCall(AudioServicePlugin.java:864)
	at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:267)
	at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:292)
	at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$io-flutter-embedding-engine-dart-DartMessenger(DartMessenger.java:319)
	at io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0.run(Unknown Source:12)
	at android.os.Handler.handleCallback(Handler.java:959)
	at android.os.Handler.dispatchMessage(Handler.java:100)<truncated: 358 chars>
       at StandardMethodCodec.decodeEnvelope(message_codecs.dart:648)
       at MethodChannel._invokeMethod(platform_channel.dart:334)
       at MethodChannelAudioService.setState(method_channel_audio_service.dart:19)
       at AudioService.init.<fn>(audio_service.dart:958)
        

@Abestanis
Copy link
Collaborator Author

As a side note - we probably would like to show title and artist in the widget, but I'm in favor of doing this in a separate PR

I personally am not a fan of having any text in the widget, but we can definiert provide a second widget with more information, or maybe even allow to configure it when creating the widget.

intent.type = keyEvent.toString()
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(context, 0, intent, flags)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking into the ANR, but we definitely need to use getForegroundService, because otherwise the intent will be ignored on newer Android versions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we might be able to work around this issue by using the broadcast receiver that listens to media playback button presses and that starts the service for us.

During my testing this worked very well, except for one time where we failed to create a notification with an ForegroundServiceStartNotAllowedException. This was after killing the app and I think it was caused by an extremely slow startup, which might have caused us to exceed some timeframe. But I have not been able to replicate that, it worked every other attempt.

Comment on lines +27 to +30
android:layout_width="match_parent"
android:layout_height="@dimen/musicPlayerWidgetButtonSize"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
android:layout_width="match_parent"
android:layout_height="@dimen/musicPlayerWidgetButtonSize"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="@dimen/musicPlayerWidgetButtonSize"
android:layout_centerHorizontal="true"

We could also not stretch the button bar over the entire width, which would look like this:

Widgets with centered button bar
More widgets with centered button bar

Would you prefer that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add home widget
2 participants