Skip to content

Now playing is not being detected #24

@fsdtmr

Description

@fsdtmr

On android 14

build.gradle.kts


plugins {
    id("com.android.application")
    id("kotlin-android")
    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
    id("dev.flutter.flutter-gradle-plugin")
}

android {
    namespace = "com.example.example"
    compileSdk = flutter.compileSdkVersion
    ndkVersion = "27.0.12077973" //flutter.ndkVersion

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_11.toString()
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "com.example.example"
        // You can update the following values to match your application needs.
        // For more information, see: https://flutter.dev/to/review-gradle-config.
        minSdk = flutter.minSdkVersion
        targetSdk = flutter.targetSdkVersion
        versionCode = flutter.versionCode
        versionName = flutter.versionName
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig = signingConfigs.getByName("debug")
        }
    }
}

flutter {
    source = "../.."
}

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />

    <application
        android:label="example"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
                android:name="io.flutter.embedding.android.NormalTheme"
                android:resource="@style/NormalTheme"
            />
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
        <service android:name="com.gomes.nowplaying.NowPlayingListenerService"
            android:label="NowPlayingListenerService"
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
            android:exported="false"
            >
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService" />
            </intent-filter>
        </service>
    </application>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT" />
            <data android:mimeType="text/plain" />
        </intent>
    </queries>
</manifest>

main.dart


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:nowplaying/nowplaying.dart';
import 'package:nowplaying/nowplaying_track.dart';
import 'package:provider/provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await NowPlaying.instance.start(
   
  );
  runApp(const NowPlayingExample());
}

class NowPlayingExample extends StatelessWidget {
  const NowPlayingExample({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('NowPlaying example app')),
        body: Center(child: NowPlayingTrackWidget()),
      ),
    );
  }
}

class NowPlayingTrackWidget extends StatefulWidget {
  @override
  _NowPlayingTrackState createState() => _NowPlayingTrackState();
}

class _NowPlayingTrackState extends State<NowPlayingTrackWidget> {
  bool _isEnabled = false;

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

  Future<void> _checkPermissions() async {
    final isEnabled = await NowPlaying.instance.isEnabled();
    setState(() {
      _isEnabled = isEnabled;
    });

    if (isEnabled) {
      if (NowPlaying.spotify.isEnabled && NowPlaying.spotify.isUnconnected) {
        // NowPlaying.spotify.signIn(context);
      }
    }
  }

  Future<void> _requestPermission() async {
    final shown = await NowPlaying.instance.requestPermissions();
    print('MANAGED TO SHOW PERMS PAGE: $shown');
    await _checkPermissions();
  }

  @override
  Widget build(BuildContext context) {
    if (!_isEnabled) {
      return Center(
        child: ElevatedButton(
          onPressed: _requestPermission,
          child: const Text('Request Notification Access'),
        ),
      );
    }

    return StreamProvider<NowPlayingTrack>.value(
      initialData: NowPlayingTrack.loading,
      value: NowPlaying.instance.stream,
      child: Consumer<NowPlayingTrack>(
        builder: (context, track, _) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              if (track.isStopped) const Text('Nothing playing'),
              if (!track.isStopped) ...[
                if (track.title != null) Text(track.title!.trim()),
                if (track.artist != null) Text(track.artist!.trim()),
                if (track.album != null) Text(track.album!.trim()),
                Text(track.duration.truncToSecond.toShortString()),
                TrackProgressIndicator(track),
                Text(track.state.toString()),
                Stack(
                  alignment: Alignment.center,
                  children: [
                    Container(
                      margin: const EdgeInsets.fromLTRB(8, 8, 8, 16),
                      width: 200,
                      height: 200,
                      alignment: Alignment.center,
                      color: Colors.grey,
                      child: AnimatedSwitcher(
                        duration: const Duration(milliseconds: 350),
                        child: _imageFrom(track),
                      ),
                    ),
                    Positioned(bottom: 0, right: 0, child: _iconFrom(track)),
                    if (track.source != null)
                      Positioned(bottom: 0, left: 8, child: Text(track.source!.trim())),
                  ],
                ),
              ],
            ],
          );
        },
      ),
    );
  }

  Widget _imageFrom(NowPlayingTrack track) {
    if (track.hasImage) {
      return Image(
        key: Key(track.id),
        image: track.image!,
        width: 200,
        height: 200,
        fit: BoxFit.contain,
      );
    }

    if (track.isResolvingImage) {
      return const SizedBox(
        width: 50.0,
        height: 50.0,
        child: CircularProgressIndicator(
          valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
        ),
      );
    }

    return const Text(
      'NO\nARTWORK\nFOUND',
      textAlign: TextAlign.center,
      style: TextStyle(fontSize: 24, color: Colors.white),
    );
  }

  Widget _iconFrom(NowPlayingTrack track) {
    if (track.hasIcon) {
      return Container(
        padding: const EdgeInsets.all(6),
        decoration: const BoxDecoration(
          color: Colors.white,
          boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black)],
          shape: BoxShape.circle,
        ),
        child: Image(
          image: track.icon!,
          width: 25,
          height: 25,
          fit: BoxFit.contain,
          color: _fgColorFor(track),
          colorBlendMode: BlendMode.srcIn,
        ),
      );
    }
    return Container();
  }

  Color _fgColorFor(NowPlayingTrack track) {
    switch (track.source) {
      case "com.apple.music":
        return Colors.blue;
      case "com.hughesmedia.big_finish":
        return Colors.red;
      case "com.spotify.music":
        return Colors.green;
      default:
        return Colors.purpleAccent;
    }
  }
}

class TrackProgressIndicator extends StatefulWidget {
  final NowPlayingTrack track;

  const TrackProgressIndicator(this.track, {super.key});

  @override
  _TrackProgressIndicatorState createState() => _TrackProgressIndicatorState();
}

class _TrackProgressIndicatorState extends State<TrackProgressIndicator> {
  late Timer _timer;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      setState(() {});
    });
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final progress = widget.track.progress.truncToSecond;
    final countdown =
        widget.track.duration - progress + const Duration(seconds: 1);

    return Column(
      children: [
        Text('Progress: ${progress.toShortString()}'),
        Text('Remaining: ${countdown.toShortString()}'),
      ],
    );
  }
}

extension DurationExtension on Duration {
  Duration get truncToSecond {
    final ms = inMilliseconds;
    return Duration(milliseconds: ms - ms % 1000);
  }

  String toShortString() => toString().split('.').first;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions