Skip to content

Commit

Permalink
Running service in foreground to play alarm sounds
Browse files Browse the repository at this point in the history
  • Loading branch information
aybefox committed Apr 12, 2020
1 parent 2d811f3 commit ff31e32
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 17 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ A simple ringtone, alarm & notification player plugin.

## Usage

Register service in AndroidManifest.xml:
Register service and add permission to AndroidManifest.xml:

```xml
<service android:name="io.inway.ringtone.player.FlutterRingtonePlayerService"/>

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
```

Add following import to your code:
Expand Down Expand Up @@ -43,7 +45,8 @@ FlutterRingtonePlayer.play(
| -------------- | ------------ |
| `bool` looping | Enables looping of ringtone. Requires `FlutterRingtonePlayer.stop();` to stop ringing. |
| `double` volume | Sets ringtone volume in range 0 to 1.0. |
| `bool` asAlarm | Allows to ignore device's silent/vibration mode and play given sound anyway. |
| `bool` asAlarm | Allows to ignore device's silent/vibration mode and play given sound anyway. Because this will run the service in foreground you also have to set `alarmNotificationMeta`. |
| `AlarmNotificationMeta` alarmNotificationMeta | Sets further attributes for the alarm notification which will be created if the sound will be played as alarm. |


To stop looped ringtone please use:
Expand Down
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ android {
}

dependencies {
api 'androidx.core:core:1.2.0'
api 'androidx.annotation:annotation:1.1.0'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.inway.ringtone.player;

import java.io.Serializable;
import java.util.Map;

public class AlarmNotificationMeta implements Serializable {
private final Map<String, Object> notificationMetaValues;

public AlarmNotificationMeta(Map<String, Object> notificationMetaValues) {
this.notificationMetaValues = notificationMetaValues;
}

public CharSequence getContentTitle() {
return (CharSequence) notificationMetaValues.get("contentTitle");
}

public CharSequence getContentText() {
return (CharSequence) notificationMetaValues.get("contentText");
}

public CharSequence getSubText() {
return (CharSequence) notificationMetaValues.get("subText");
}

public String getIconDrawableResourceName() {
return (String) notificationMetaValues.get("iconDrawableResourceName");
}

public String getActivityClassLaunchedByIntent() {
return (String) notificationMetaValues.get("activityClassLaunchedByIntent");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;

import java.util.Map;

/**
* FlutterRingtonePlayerPlugin
*/
Expand Down Expand Up @@ -41,7 +44,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
result.success(null);
}
} catch (Exception e) {
e.printStackTrace();
result.error("Exception", e.getMessage(), null);
}
}
Expand All @@ -59,13 +61,31 @@ private RingtoneMeta createRingtoneMeta(MethodCall call) {
if (volume != null) {
meta.setVolume(volume.floatValue());
}

if (meta.getAsAlarm()) {
final String alarmNotificationMetaKey = "alarmNotificationMeta";

if (call.hasArgument(alarmNotificationMetaKey)) {
final Map<String, Object> notificationMetaValues = getMethodCallArgument(call, alarmNotificationMetaKey, Map.class);
final AlarmNotificationMeta notificationMeta = new AlarmNotificationMeta(notificationMetaValues);
meta.setAlarmNotificationMeta(notificationMeta);
} else {
throw new IllegalArgumentException("if asAlarm=true you have to deliver '" + alarmNotificationMetaKey + "'");
}
}

return meta;
}

private void startRingtone(RingtoneMeta meta) {
final Intent intent = createServiceIntent();
intent.putExtra(FlutterRingtonePlayerService.RINGTONE_META_INTENT_EXTRA_KEY, meta);
context.startService(intent);

if (meta.getAsAlarm()) {
ContextCompat.startForegroundService(context, intent);
} else {
context.startService(intent);
}
}

private void stopRingtone() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package io.inway.ringtone.player;

import android.app.Service;
import android.app.*;
import android.content.Intent;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
Expand All @@ -10,9 +11,11 @@
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

public class FlutterRingtonePlayerService extends Service {
public static final String RINGTONE_META_INTENT_EXTRA_KEY = "ringtone-meta";
private static final String CHANNEL_ID = "flutter-ringtone-player-service-channel";

private Ringtone ringtone;

Expand All @@ -26,19 +29,25 @@ public IBinder onBind(Intent intent) {
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
final Bundle extras = intent.getExtras();

if (extras == null) {
throwInvalidArgumentsException();
}

final RingtoneMeta meta = (RingtoneMeta) extras.getSerializable(RINGTONE_META_INTENT_EXTRA_KEY);

if (meta == null) {
throwInvalidArgumentsException();
}

if (meta.getAsAlarm()) {
startForeground(meta);
} else {
stopForeground(true);
}

stopRingtone();
startRingtone(meta);
} else {
stopSelf();
}

return super.onStartCommand(intent, flags, startId);
Expand All @@ -54,6 +63,28 @@ private void throwInvalidArgumentsException() {
throw new IllegalArgumentException("Invalid arguments given");
}

private void startForeground(RingtoneMeta ringtoneMeta) {
createNotificationChannel();

final AlarmNotificationMeta notificationMeta = ringtoneMeta.getAlarmNotificationMeta();
validate(notificationMeta);

final Class<?> activityClass = getActivityClassLaunchedByNotificationIntent(ringtoneMeta);
final Intent notificationIntent = new Intent(this, activityClass);
final int iconDrawableResourceId = getResources().getIdentifier(notificationMeta.getIconDrawableResourceName(), "drawable", getPackageName());
final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

final Notification notification = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(iconDrawableResourceId)
.setContentTitle(notificationMeta.getContentTitle())
.setContentText(notificationMeta.getContentText())
.setSubText(notificationMeta.getSubText())
.setContentIntent(pendingIntent)
.build();

startForeground(1, notification);
}

private void stopRingtone() {
if (ringtone != null) {
ringtone.stop();
Expand All @@ -66,17 +97,44 @@ private void startRingtone(RingtoneMeta meta) {
ringtone.play();
}

private void createNotificationChannel() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
final NotificationChannel serviceChannel = new NotificationChannel(CHANNEL_ID, "Foreground service channel",
NotificationManager.IMPORTANCE_DEFAULT);

serviceChannel.setLightColor(Color.RED);
serviceChannel.enableLights(true);
final NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(serviceChannel);
}
}

private void validate(AlarmNotificationMeta meta) {
if (meta.getActivityClassLaunchedByIntent() == null || meta.getIconDrawableResourceName() == null) {
throwInvalidArgumentsException();
}
}

private Class<?> getActivityClassLaunchedByNotificationIntent(RingtoneMeta ringtoneMeta) {
final String className = ringtoneMeta.getAlarmNotificationMeta().getActivityClassLaunchedByIntent();
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Class '" + className + "' not found");
}
}

private Ringtone getConfiguredRingtone(RingtoneMeta meta) {
final Uri uri = getRingtoneUri(meta.getKind());
final Ringtone ringtone = RingtoneManager.getRingtone(this, uri);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ringtone.setLooping(Boolean.TRUE.equals(meta.getLooping()));
ringtone.setLooping(meta.getLooping());
if (meta.getVolume() != null) {
ringtone.setVolume(meta.getVolume());
}
}
if (Boolean.TRUE.equals(meta.getAsAlarm())) {
if (meta.getAsAlarm()) {
ringtone.setStreamType(AudioManager.STREAM_ALARM);
}

Expand Down
17 changes: 13 additions & 4 deletions android/src/main/java/io/inway/ringtone/player/RingtoneMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class RingtoneMeta implements Serializable {
private Float volume;
private Boolean looping;
private Boolean asAlarm;
private AlarmNotificationMeta alarmNotificationMeta;

public void setKind(int kind) {
this.kind = kind;
Expand All @@ -28,15 +29,23 @@ public void setLooping(Boolean looping) {
this.looping = looping;
}

public Boolean getLooping() {
return looping;
public boolean getLooping() {
return Boolean.TRUE.equals(looping);
}

public void setAsAlarm(Boolean asAlarm) {
this.asAlarm = asAlarm;
}

public Boolean getAsAlarm() {
return asAlarm;
public boolean getAsAlarm() {
return Boolean.TRUE.equals(asAlarm);
}

public AlarmNotificationMeta getAlarmNotificationMeta() {
return alarmNotificationMeta;
}

public void setAlarmNotificationMeta(AlarmNotificationMeta alarmNotificationMeta) {
this.alarmNotificationMeta = alarmNotificationMeta;
}
}
3 changes: 3 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<application
android:name="io.flutter.app.FlutterApplication"
android:label="flutter_ringtone_player_example"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#0085F4"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M22,5.72l-4.6,-3.86 -1.29,1.53 4.6,3.86L22,5.72zM7.88,3.39L6.6,1.86 2,5.71l1.29,1.53 4.59,-3.85zM12.5,8L11,8v6l4.75,2.85 0.75,-1.23 -4,-2.37L12.5,8zM12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>
3 changes: 3 additions & 0 deletions example/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true
6 changes: 5 additions & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_ringtone_player/alarm_notification_meta.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';

void main() => runApp(MyApp());
Expand All @@ -19,7 +20,10 @@ class MyApp extends StatelessWidget {
child: RaisedButton(
child: const Text('playAlarm'),
onPressed: () {
FlutterRingtonePlayer.playAlarm();
FlutterRingtonePlayer.playAlarm(
alarmNotificationMeta: AlarmNotificationMeta(
'io.inway.ringtone.player_example.MainActivity', 'ic_alarm_notification',
contentTitle: 'Alarm', contentText: 'Alarm is active', subText: 'Subtext'));
},
),
),
Expand Down
18 changes: 18 additions & 0 deletions lib/alarm_notification_meta.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class AlarmNotificationMeta {
final String activityClassLaunchedByIntent;
final String iconDrawableResourceName;
final String contentTitle;
final String contentText;
final String subText;

AlarmNotificationMeta(this.activityClassLaunchedByIntent, this.iconDrawableResourceName,
{this.contentTitle, this.contentText, this.subText});

Map<String, dynamic> toMap() => {
'activityClassLaunchedByIntent': activityClassLaunchedByIntent,
'iconDrawableResourceName': iconDrawableResourceName,
'contentTitle': contentTitle,
'contentText': contentText,
'subText': subText,
};
}
13 changes: 10 additions & 3 deletions lib/flutter_ringtone_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ringtone_player/alarm_notification_meta.dart';

import 'android_sounds.dart';
import 'ios_sounds.dart';
Expand All @@ -27,6 +28,8 @@ class FlutterRingtonePlayer {
/// [asAlarm] is an Android only flag that lets play given sound
/// as an alarm, that is, phone will make sound even if
/// it is in silent or vibration mode.
/// If sound is played as alarm the plugin will run the service in foreground.
/// Therefore you also have to set [alarmNotificationMeta].
///
/// See also:
/// * [AndroidSounds]
Expand All @@ -36,7 +39,8 @@ class FlutterRingtonePlayer {
@required IosSound ios,
double volume,
bool looping,
bool asAlarm}) async {
bool asAlarm,
AlarmNotificationMeta alarmNotificationMeta}) async {
try {
var args = <String, dynamic>{
'android': android.value,
Expand All @@ -45,20 +49,23 @@ class FlutterRingtonePlayer {
if (looping != null) args['looping'] = looping;
if (volume != null) args['volume'] = volume;
if (asAlarm != null) args['asAlarm'] = asAlarm;
if (alarmNotificationMeta != null) args['alarmNotificationMeta'] = alarmNotificationMeta.toMap();

_channel.invokeMethod('play', args);
} on PlatformException {}
}

/// Play default alarm sound (looping on Android)
static Future<void> playAlarm(
{double volume, bool looping = true, bool asAlarm = true}) async =>
{double volume, bool looping = true, bool asAlarm = true, AlarmNotificationMeta alarmNotificationMeta}) async =>
play(
android: AndroidSounds.alarm,
ios: IosSounds.alarm,
volume: volume,
looping: looping,
asAlarm: asAlarm);
asAlarm: asAlarm,
alarmNotificationMeta: alarmNotificationMeta,
);

/// Play default notification sound
static Future<void> playNotification(
Expand Down

0 comments on commit ff31e32

Please sign in to comment.