diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 455229a5..f8b6fcd3 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -28,6 +28,9 @@ jobs: - name: Install dependencies run: flutter pub get + - name: Analyze + run: flutter analyze + - name: Run unit tests with coverage run: flutter test --coverage diff --git a/coverage/html/amber.png b/coverage/html/amber.png new file mode 100644 index 00000000..2cab170d Binary files /dev/null and b/coverage/html/amber.png differ diff --git a/coverage/html/cmd_line b/coverage/html/cmd_line new file mode 100644 index 00000000..e3db9d3c --- /dev/null +++ b/coverage/html/cmd_line @@ -0,0 +1 @@ +genhtml coverage/lcov.info -o coverage/html --no-function-coverage diff --git a/coverage/html/emerald.png b/coverage/html/emerald.png new file mode 100644 index 00000000..38ad4f40 Binary files /dev/null and b/coverage/html/emerald.png differ diff --git a/coverage/html/gcov.css b/coverage/html/gcov.css new file mode 100644 index 00000000..1cacc835 --- /dev/null +++ b/coverage/html/gcov.css @@ -0,0 +1,1125 @@ +/* All views: initial background and text color */ +body +{ + color: #000000; + background-color: #ffffff; +} + +/* All views: standard link format*/ +a:link +{ + color: #284fa8; + text-decoration: underline; +} + +/* All views: standard link - visited format */ +a:visited +{ + color: #00cb40; + text-decoration: underline; +} + +/* All views: standard link - activated format */ +a:active +{ + color: #ff0040; + text-decoration: underline; +} + +/* All views: main title format */ +td.title +{ + text-align: center; + padding-bottom: 10px; + font-family: sans-serif; + font-size: 20pt; + font-style: italic; + font-weight: bold; +} +/* table footnote */ +td.footnote +{ + text-align: left; + padding-left: 100px; + padding-right: 10px; + background-color: #dae7fe; /* light blue table background color */ + /* dark blue table header color + background-color: #6688d4; */ + white-space: nowrap; + font-family: sans-serif; + font-style: italic; + font-size:70%; +} +/* "Line coverage date bins" leader */ +td.subTableHeader +{ + text-align: center; + padding-bottom: 6px; + font-family: sans-serif; + font-weight: bold; + vertical-align: center; +} + +/* All views: header item format */ +td.headerItem +{ + text-align: right; + padding-right: 6px; + font-family: sans-serif; + font-weight: bold; + vertical-align: top; + white-space: nowrap; +} + +/* All views: header item value format */ +td.headerValue +{ + text-align: left; + color: #284fa8; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; +} + +/* All views: header item coverage table heading */ +td.headerCovTableHead +{ + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; +} + +/* All views: header item coverage table entry */ +td.headerCovTableEntry +{ + text-align: right; + color: #284fa8; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #dae7fe; +} + +/* All views: header item coverage table entry for high coverage rate */ +td.headerCovTableEntryHi +{ + text-align: right; + color: #000000; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #a7fc9d; +} + +/* All views: header item coverage table entry for medium coverage rate */ +td.headerCovTableEntryMed +{ + text-align: right; + color: #000000; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #ffea20; +} + +/* All views: header item coverage table entry for ow coverage rate */ +td.headerCovTableEntryLo +{ + text-align: right; + color: #000000; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #ff0000; +} + +/* All views: header legend value for legend entry */ +td.headerValueLeg +{ + text-align: left; + color: #000000; + font-family: sans-serif; + font-size: 80%; + white-space: nowrap; + padding-top: 4px; +} + +/* All views: color of horizontal ruler */ +td.ruler +{ + background-color: #6688d4; +} + +/* All views: version string format */ +td.versionInfo +{ + text-align: center; + padding-top: 2px; + font-family: sans-serif; + font-style: italic; +} + +/* Directory view/File view (all)/Test case descriptions: + table headline format */ +td.tableHead +{ + text-align: center; + color: #ffffff; + background-color: #6688d4; + font-family: sans-serif; + font-size: 120%; + font-weight: bold; + white-space: nowrap; + padding-left: 4px; + padding-right: 4px; +} + +span.tableHeadSort +{ + padding-right: 4px; +} + +/* Directory view/File view (all): filename entry format */ +td.coverFile +{ + text-align: left; + padding-left: 10px; + padding-right: 20px; + color: #284fa8; + background-color: #dae7fe; + font-family: monospace; +} + +/* Directory view/File view (all): directory name entry format */ +td.coverDirectory +{ + text-align: left; + padding-left: 10px; + padding-right: 20px; + color: #284fa8; + background-color: #b8d0ff; + font-family: monospace; +} + +/* Directory view/File view (all): filename entry format */ +td.overallOwner +{ + text-align: center; + font-weight: bold; + font-family: sans-serif; + background-color: #dae7fe; + padding-right: 10px; + padding-left: 10px; +} + +/* Directory view/File view (all): filename entry format */ +td.ownerName +{ + text-align: right; + font-style: italic; + font-family: sans-serif; + background-color: #E5DBDB; + padding-right: 10px; + padding-left: 20px; +} + +/* Directory view/File view (all): bar-graph entry format*/ +td.coverBar +{ + padding-left: 10px; + padding-right: 10px; + background-color: #dae7fe; +} + +/* Directory view/File view (all): bar-graph entry format*/ +td.owner_coverBar +{ + padding-left: 10px; + padding-right: 10px; + background-color: #E5DBDB; +} + +/* Directory view/File view (all): bar-graph outline color */ +td.coverBarOutline +{ + background-color: #000000; +} + +/* Directory view/File view (all): percentage entry for files with + high coverage rate */ +td.coverPerHi +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #a7fc9d; + font-weight: bold; + font-family: sans-serif; +} + +/* 'owner' entry: slightly lighter color than 'coverPerHi' */ +td.owner_coverPerHi +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #82E0AA; + font-weight: bold; + font-family: sans-serif; +} + +/* Directory view/File view (all): line count entry */ +td.coverNumDflt +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #dae7fe; + white-space: nowrap; + font-family: sans-serif; +} + +/* td background color and font for the 'owner' section of the table */ +td.ownerTla +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #E5DBDB; + white-space: nowrap; + font-family: sans-serif; + font-style: italic; +} + +/* Directory view/File view (all): line count entry for files with + high coverage rate */ +td.coverNumHi +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #a7fc9d; + white-space: nowrap; + font-family: sans-serif; +} + +td.owner_coverNumHi +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #82E0AA; + white-space: nowrap; + font-family: sans-serif; +} + +/* Directory view/File view (all): percentage entry for files with + medium coverage rate */ +td.coverPerMed +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #ffea20; + font-weight: bold; + font-family: sans-serif; +} + +td.owner_coverPerMed +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #F9E79F; + font-weight: bold; + font-family: sans-serif; +} + +/* Directory view/File view (all): line count entry for files with + medium coverage rate */ +td.coverNumMed +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #ffea20; + white-space: nowrap; + font-family: sans-serif; +} + +td.owner_coverNumMed +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #F9E79F; + white-space: nowrap; + font-family: sans-serif; +} + +/* Directory view/File view (all): percentage entry for files with + low coverage rate */ +td.coverPerLo +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #ff0000; + font-weight: bold; + font-family: sans-serif; +} + +td.owner_coverPerLo +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #EC7063; + font-weight: bold; + font-family: sans-serif; +} + +/* Directory view/File view (all): line count entry for files with + low coverage rate */ +td.coverNumLo +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #ff0000; + white-space: nowrap; + font-family: sans-serif; +} + +td.owner_coverNumLo +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #EC7063; + white-space: nowrap; + font-family: sans-serif; +} + +/* File view (all): "show/hide details" link format */ +a.detail:link +{ + color: #b8d0ff; + font-size:80%; +} + +/* File view (all): "show/hide details" link - visited format */ +a.detail:visited +{ + color: #b8d0ff; + font-size:80%; +} + +/* File view (all): "show/hide details" link - activated format */ +a.detail:active +{ + color: #ffffff; + font-size:80%; +} + +/* File view (detail): test name entry */ +td.testName +{ + text-align: right; + padding-right: 10px; + background-color: #dae7fe; + font-family: sans-serif; +} + +/* File view (detail): test percentage entry */ +td.testPer +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #dae7fe; + font-family: sans-serif; +} + +/* File view (detail): test lines count entry */ +td.testNum +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #dae7fe; + font-family: sans-serif; +} + +/* Test case descriptions: test name format*/ +dt +{ + font-family: sans-serif; + font-weight: bold; +} + +/* Test case descriptions: description table body */ +td.testDescription +{ + padding-top: 10px; + padding-left: 30px; + padding-bottom: 10px; + padding-right: 30px; + background-color: #dae7fe; +} + +/* Source code view: function entry */ +td.coverFn +{ + text-align: left; + padding-left: 10px; + padding-right: 20px; + color: #284fa8; + background-color: #dae7fe; + font-family: monospace; +} + +/* Source code view: function entry zero count*/ +td.coverFnLo +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #ff0000; + font-weight: bold; + font-family: sans-serif; +} + +/* Source code view: function entry nonzero count*/ +td.coverFnHi +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #dae7fe; + font-weight: bold; + font-family: sans-serif; +} + +td.coverFnAlias +{ + text-align: right; + padding-left: 10px; + padding-right: 20px; + color: #284fa8; + /* make this a slightly different color than the leader - otherwise, + otherwise the alias is hard to distinguish in the table */ + background-color: #E5DBDB; /* very light pale grey/blue */ + font-family: monospace; +} + +/* Source code view: function entry zero count*/ +td.coverFnAliasLo +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #EC7063; /* lighter red */ + font-family: sans-serif; +} + +/* Source code view: function entry nonzero count*/ +td.coverFnAliasHi +{ + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #dae7fe; + font-weight: bold; + font-family: sans-serif; +} + +/* Source code view: source code format */ +pre.source +{ + font-family: monospace; + white-space: pre; + margin-top: 2px; +} + +/* elided/removed code */ +span.elidedSource +{ + font-family: sans-serif; + /*font-size: 8pt; */ + font-style: italic; + background-color: lightgrey; +} + +/* Source code view: line number format */ +span.lineNum +{ + background-color: #efe383; +} + +/* Source code view: line number format when there are deleted + lines in the corresponding location */ +span.lineNumWithDelete +{ + foreground-color: #efe383; + background-color: lightgrey; +} + +/* Source code view: format for Cov legend */ +span.coverLegendCov +{ + padding-left: 10px; + padding-right: 10px; + padding-bottom: 2px; + background-color: #cad7fe; +} + +/* Source code view: format for NoCov legend */ +span.coverLegendNoCov +{ + padding-left: 10px; + padding-right: 10px; + padding-bottom: 2px; + background-color: #ff6230; +} + +/* Source code view: format for the source code heading line */ +pre.sourceHeading +{ + white-space: pre; + font-family: monospace; + font-weight: bold; + margin: 0px; +} + +/* All views: header legend value for low rate */ +td.headerValueLegL +{ + font-family: sans-serif; + text-align: center; + white-space: nowrap; + padding-left: 4px; + padding-right: 2px; + background-color: #ff0000; + font-size: 80%; +} + +/* All views: header legend value for med rate */ +td.headerValueLegM +{ + font-family: sans-serif; + text-align: center; + white-space: nowrap; + padding-left: 2px; + padding-right: 2px; + background-color: #ffea20; + font-size: 80%; +} + +/* All views: header legend value for hi rate */ +td.headerValueLegH +{ + font-family: sans-serif; + text-align: center; + white-space: nowrap; + padding-left: 2px; + padding-right: 4px; + background-color: #a7fc9d; + font-size: 80%; +} + +/* All views except source code view: legend format for low coverage */ +span.coverLegendCovLo +{ + padding-left: 10px; + padding-right: 10px; + padding-top: 2px; + background-color: #ff0000; +} + +/* All views except source code view: legend format for med coverage */ +span.coverLegendCovMed +{ + padding-left: 10px; + padding-right: 10px; + padding-top: 2px; + background-color: #ffea20; +} + +/* All views except source code view: legend format for hi coverage */ +span.coverLegendCovHi +{ + padding-left: 10px; + padding-right: 10px; + padding-top: 2px; + background-color: #a7fc9d; +} + +a.branchTla:link +{ + color: #000000; +} + +a.branchTla:visited +{ + color: #000000; +} + +a.mcdcTla:link +{ + color: #000000; +} + +a.mcdcTla:visited +{ + color: #000000; +} + +/* Source code view/table entry background: format for lines classified as "Uncovered New Code (+ => 0): +Newly added code is not tested" */ +td.tlaUNC +{ + text-align: right; + background-color: #FF6230; +} +td.tlaBgUNC { + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Uncovered New Code (+ => 0): +Newly added code is not tested" */ +span.tlaUNC +{ + text-align: left; + background-color: #FF6230; +} +span.tlaBgUNC { + background-color: #FF6230; +} +a.tlaBgUNC { + background-color: #FF6230; + color: #000000; +} + +td.headerCovTableHeadUNC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Lost Baseline Coverage (1 => 0): +Unchanged code is no longer tested" */ +td.tlaLBC +{ + text-align: right; + background-color: #FF6230; +} +td.tlaBgLBC { + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Lost Baseline Coverage (1 => 0): +Unchanged code is no longer tested" */ +span.tlaLBC +{ + text-align: left; + background-color: #FF6230; +} +span.tlaBgLBC { + background-color: #FF6230; +} +a.tlaBgLBC { + background-color: #FF6230; + color: #000000; +} + +td.headerCovTableHeadLBC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Uncovered Included Code (# => 0): +Previously unused code is untested" */ +td.tlaUIC +{ + text-align: right; + background-color: #FF6230; +} +td.tlaBgUIC { + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Uncovered Included Code (# => 0): +Previously unused code is untested" */ +span.tlaUIC +{ + text-align: left; + background-color: #FF6230; +} +span.tlaBgUIC { + background-color: #FF6230; +} +a.tlaBgUIC { + background-color: #FF6230; + color: #000000; +} + +td.headerCovTableHeadUIC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Uncovered Baseline Code (0 => 0): +Unchanged code was untested before, is untested now" */ +td.tlaUBC +{ + text-align: right; + background-color: #FF6230; +} +td.tlaBgUBC { + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Uncovered Baseline Code (0 => 0): +Unchanged code was untested before, is untested now" */ +span.tlaUBC +{ + text-align: left; + background-color: #FF6230; +} +span.tlaBgUBC { + background-color: #FF6230; +} +a.tlaBgUBC { + background-color: #FF6230; + color: #000000; +} + +td.headerCovTableHeadUBC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FF6230; +} + +/* Source code view/table entry background: format for lines classified as "Gained Baseline Coverage (0 => 1): +Unchanged code is tested now" */ +td.tlaGBC +{ + text-align: right; + background-color: #CAD7FE; +} +td.tlaBgGBC { + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Gained Baseline Coverage (0 => 1): +Unchanged code is tested now" */ +span.tlaGBC +{ + text-align: left; + background-color: #CAD7FE; +} +span.tlaBgGBC { + background-color: #CAD7FE; +} +a.tlaBgGBC { + background-color: #CAD7FE; + color: #000000; +} + +td.headerCovTableHeadGBC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Gained Included Coverage (# => 1): +Previously unused code is tested now" */ +td.tlaGIC +{ + text-align: right; + background-color: #CAD7FE; +} +td.tlaBgGIC { + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Gained Included Coverage (# => 1): +Previously unused code is tested now" */ +span.tlaGIC +{ + text-align: left; + background-color: #CAD7FE; +} +span.tlaBgGIC { + background-color: #CAD7FE; +} +a.tlaBgGIC { + background-color: #CAD7FE; + color: #000000; +} + +td.headerCovTableHeadGIC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Gained New Coverage (+ => 1): +Newly added code is tested" */ +td.tlaGNC +{ + text-align: right; + background-color: #CAD7FE; +} +td.tlaBgGNC { + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Gained New Coverage (+ => 1): +Newly added code is tested" */ +span.tlaGNC +{ + text-align: left; + background-color: #CAD7FE; +} +span.tlaBgGNC { + background-color: #CAD7FE; +} +a.tlaBgGNC { + background-color: #CAD7FE; + color: #000000; +} + +td.headerCovTableHeadGNC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Covered Baseline Code (1 => 1): +Unchanged code was tested before and is still tested" */ +td.tlaCBC +{ + text-align: right; + background-color: #CAD7FE; +} +td.tlaBgCBC { + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Covered Baseline Code (1 => 1): +Unchanged code was tested before and is still tested" */ +span.tlaCBC +{ + text-align: left; + background-color: #CAD7FE; +} +span.tlaBgCBC { + background-color: #CAD7FE; +} +a.tlaBgCBC { + background-color: #CAD7FE; + color: #000000; +} + +td.headerCovTableHeadCBC { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #CAD7FE; +} + +/* Source code view/table entry background: format for lines classified as "Excluded Uncovered Baseline (0 => #): +Previously untested code is unused now" */ +td.tlaEUB +{ + text-align: right; + background-color: #FFFFFF; +} +td.tlaBgEUB { + background-color: #FFFFFF; +} + +/* Source code view/table entry background: format for lines classified as "Excluded Uncovered Baseline (0 => #): +Previously untested code is unused now" */ +span.tlaEUB +{ + text-align: left; + background-color: #FFFFFF; +} +span.tlaBgEUB { + background-color: #FFFFFF; +} +a.tlaBgEUB { + background-color: #FFFFFF; + color: #000000; +} + +td.headerCovTableHeadEUB { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FFFFFF; +} + +/* Source code view/table entry background: format for lines classified as "Excluded Covered Baseline (1 => #): +Previously tested code is unused now" */ +td.tlaECB +{ + text-align: right; + background-color: #FFFFFF; +} +td.tlaBgECB { + background-color: #FFFFFF; +} + +/* Source code view/table entry background: format for lines classified as "Excluded Covered Baseline (1 => #): +Previously tested code is unused now" */ +span.tlaECB +{ + text-align: left; + background-color: #FFFFFF; +} +span.tlaBgECB { + background-color: #FFFFFF; +} +a.tlaBgECB { + background-color: #FFFFFF; + color: #000000; +} + +td.headerCovTableHeadECB { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FFFFFF; +} + +/* Source code view/table entry background: format for lines classified as "Deleted Uncovered Baseline (0 => -): +Previously untested code has been deleted" */ +td.tlaDUB +{ + text-align: right; + background-color: #FFFFFF; +} +td.tlaBgDUB { + background-color: #FFFFFF; +} + +/* Source code view/table entry background: format for lines classified as "Deleted Uncovered Baseline (0 => -): +Previously untested code has been deleted" */ +span.tlaDUB +{ + text-align: left; + background-color: #FFFFFF; +} +span.tlaBgDUB { + background-color: #FFFFFF; +} +a.tlaBgDUB { + background-color: #FFFFFF; + color: #000000; +} + +td.headerCovTableHeadDUB { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FFFFFF; +} + +/* Source code view/table entry background: format for lines classified as "Deleted Covered Baseline (1 => -): +Previously tested code has been deleted" */ +td.tlaDCB +{ + text-align: right; + background-color: #FFFFFF; +} +td.tlaBgDCB { + background-color: #FFFFFF; +} + +/* Source code view/table entry background: format for lines classified as "Deleted Covered Baseline (1 => -): +Previously tested code has been deleted" */ +span.tlaDCB +{ + text-align: left; + background-color: #FFFFFF; +} +span.tlaBgDCB { + background-color: #FFFFFF; +} +a.tlaBgDCB { + background-color: #FFFFFF; + color: #000000; +} + +td.headerCovTableHeadDCB { + text-align: center; + padding-right: 6px; + padding-left: 6px; + padding-bottom: 0px; + font-family: sans-serif; + white-space: nowrap; + background-color: #FFFFFF; +} + +/* Source code view: format for date/owner bin that is not hit */ +span.missBins +{ + background-color: #ff0000 /* red */ +} diff --git a/coverage/html/glass.png b/coverage/html/glass.png new file mode 100644 index 00000000..e1abc006 Binary files /dev/null and b/coverage/html/glass.png differ diff --git a/coverage/html/index-sort-l.html b/coverage/html/index-sort-l.html new file mode 100644 index 00000000..15ec7922 --- /dev/null +++ b/coverage/html/index-sort-l.html @@ -0,0 +1,127 @@ + + + + + + + LCOV - lcov.info + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top levelCoverageTotalHit
Test:lcov.infoLines:97.9 %577565
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Directory Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
lib/ +
97.1%97.1%
+
97.1 %384373
lib/models/ +
99.4%99.4%
+
99.4 %157156
lib/errors/ +
100.0%
+
100.0 %1212
lib/ui/ +
100.0%
+
100.0 %1212
lib/utils/ +
100.0%
+
100.0 %1212
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/index.html b/coverage/html/index.html new file mode 100644 index 00000000..7a821487 --- /dev/null +++ b/coverage/html/index.html @@ -0,0 +1,127 @@ + + + + + + + LCOV - lcov.info + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top levelCoverageTotalHit
Test:lcov.infoLines:97.9 %577565
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Directory Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
lib/ +
97.1%97.1%
+
97.1 %384373
lib/errors/ +
100.0%
+
100.0 %1212
lib/models/ +
99.4%99.4%
+
99.4 %157156
lib/ui/ +
100.0%
+
100.0 %1212
lib/utils/ +
100.0%
+
100.0 %1212
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/errors/index-sort-l.html b/coverage/html/lib/errors/index-sort-l.html new file mode 100644 index 00000000..2c6c1e36 --- /dev/null +++ b/coverage/html/lib/errors/index-sort-l.html @@ -0,0 +1,118 @@ + + + + + + + LCOV - lcov.info - lib/errors + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/errorsCoverageTotalHit
Test:lcov.infoLines:100.0 %1212
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
service_already_started_exception.dart +
100.0%
+
100.0 %33
service_not_initialized_exception.dart +
100.0%
+
100.0 %33
service_not_started_exception.dart +
100.0%
+
100.0 %33
service_timeout_exception.dart +
100.0%
+
100.0 %33
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/errors/index.html b/coverage/html/lib/errors/index.html new file mode 100644 index 00000000..870766bd --- /dev/null +++ b/coverage/html/lib/errors/index.html @@ -0,0 +1,118 @@ + + + + + + + LCOV - lcov.info - lib/errors + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/errorsCoverageTotalHit
Test:lcov.infoLines:100.0 %1212
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
service_already_started_exception.dart +
100.0%
+
100.0 %33
service_not_initialized_exception.dart +
100.0%
+
100.0 %33
service_not_started_exception.dart +
100.0%
+
100.0 %33
service_timeout_exception.dart +
100.0%
+
100.0 %33
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/errors/service_already_started_exception.dart.gcov.html b/coverage/html/lib/errors/service_already_started_exception.dart.gcov.html new file mode 100644 index 00000000..92b23062 --- /dev/null +++ b/coverage/html/lib/errors/service_already_started_exception.dart.gcov.html @@ -0,0 +1,81 @@ + + + + + + + LCOV - lcov.info - lib/errors/service_already_started_exception.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/errors - service_already_started_exception.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %33
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : class ServiceAlreadyStartedException implements Exception {
+       2            2 :   ServiceAlreadyStartedException(
+       3              :       [this.message = 'The service has already started.']);
+       4              : 
+       5              :   final String message;
+       6              : 
+       7            1 :   @override
+       8            2 :   String toString() => 'ServiceAlreadyStartedException: $message';
+       9              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/errors/service_not_initialized_exception.dart.gcov.html b/coverage/html/lib/errors/service_not_initialized_exception.dart.gcov.html new file mode 100644 index 00000000..d8439607 --- /dev/null +++ b/coverage/html/lib/errors/service_not_initialized_exception.dart.gcov.html @@ -0,0 +1,82 @@ + + + + + + + LCOV - lcov.info - lib/errors/service_not_initialized_exception.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/errors - service_not_initialized_exception.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %33
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : class ServiceNotInitializedException implements Exception {
+       2            2 :   ServiceNotInitializedException(
+       3              :       [this.message =
+       4              :           'Not initialized. Please call this function after calling the init function.']);
+       5              : 
+       6              :   final String message;
+       7              : 
+       8            1 :   @override
+       9            2 :   String toString() => 'ServiceNotInitializedException: $message';
+      10              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/errors/service_not_started_exception.dart.gcov.html b/coverage/html/lib/errors/service_not_started_exception.dart.gcov.html new file mode 100644 index 00000000..9b8ede88 --- /dev/null +++ b/coverage/html/lib/errors/service_not_started_exception.dart.gcov.html @@ -0,0 +1,80 @@ + + + + + + + LCOV - lcov.info - lib/errors/service_not_started_exception.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/errors - service_not_started_exception.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %33
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : class ServiceNotStartedException implements Exception {
+       2            2 :   ServiceNotStartedException([this.message = 'The service is not started.']);
+       3              : 
+       4              :   final String message;
+       5              : 
+       6            1 :   @override
+       7            2 :   String toString() => 'ServiceNotStartedException: $message';
+       8              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/errors/service_timeout_exception.dart.gcov.html b/coverage/html/lib/errors/service_timeout_exception.dart.gcov.html new file mode 100644 index 00000000..48d4428b --- /dev/null +++ b/coverage/html/lib/errors/service_timeout_exception.dart.gcov.html @@ -0,0 +1,82 @@ + + + + + + + LCOV - lcov.info - lib/errors/service_timeout_exception.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/errors - service_timeout_exception.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %33
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : class ServiceTimeoutException implements Exception {
+       2            2 :   ServiceTimeoutException(
+       3              :       [this.message =
+       4              :           'The service request timed out. (ref: https://developer.android.com/guide/components/services#StartingAService)']);
+       5              : 
+       6              :   final String message;
+       7              : 
+       8            1 :   @override
+       9            2 :   String toString() => 'ServiceTimeoutException: $message';
+      10              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/flutter_foreground_task.dart.gcov.html b/coverage/html/lib/flutter_foreground_task.dart.gcov.html new file mode 100644 index 00000000..3bb7e0f5 --- /dev/null +++ b/coverage/html/lib/flutter_foreground_task.dart.gcov.html @@ -0,0 +1,424 @@ + + + + + + + LCOV - lcov.info - lib/flutter_foreground_task.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib - flutter_foreground_task.dartCoverageTotalHit
Test:lcov.infoLines:97.9 %9795
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'dart:async';
+       2              : import 'dart:isolate';
+       3              : 
+       4              : import 'package:flutter/widgets.dart';
+       5              : 
+       6              : import 'flutter_foreground_task_controller.dart';
+       7              : import 'flutter_foreground_task_platform_interface.dart';
+       8              : import 'models/foreground_service_types.dart';
+       9              : import 'models/foreground_task_options.dart';
+      10              : import 'models/notification_button.dart';
+      11              : import 'models/notification_icon.dart';
+      12              : import 'models/notification_options.dart';
+      13              : import 'models/notification_permission.dart';
+      14              : import 'models/service_request_result.dart';
+      15              : import 'task_handler.dart';
+      16              : 
+      17              : export 'errors/service_already_started_exception.dart';
+      18              : export 'errors/service_not_initialized_exception.dart';
+      19              : export 'errors/service_not_started_exception.dart';
+      20              : export 'errors/service_timeout_exception.dart';
+      21              : export 'flutter_foreground_task_controller.dart';
+      22              : export 'models/foreground_service_types.dart';
+      23              : export 'models/foreground_task_event_action.dart';
+      24              : export 'models/foreground_task_options.dart';
+      25              : export 'models/notification_button.dart';
+      26              : export 'models/notification_channel_importance.dart';
+      27              : export 'models/notification_icon.dart';
+      28              : export 'models/notification_options.dart';
+      29              : export 'models/notification_permission.dart';
+      30              : export 'models/notification_priority.dart';
+      31              : export 'models/notification_visibility.dart';
+      32              : export 'models/service_request_result.dart';
+      33              : export 'ui/with_foreground_task.dart';
+      34              : export 'task_handler.dart';
+      35              : 
+      36              : typedef DataCallback = void Function(Object data);
+      37              : 
+      38              : /// A class that implements foreground task and provides useful utilities.
+      39              : ///
+      40              : /// All service-related static methods delegate to the `"default"` instance
+      41              : /// of [FlutterForegroundTaskController]. For multi-service usage, obtain a
+      42              : /// controller with [FlutterForegroundTaskController.of].
+      43              : class FlutterForegroundTask {
+      44              :   // The default controller backing this static API.
+      45            6 :   static FlutterForegroundTaskController get _default =>
+      46            6 :       FlutterForegroundTaskController.of('default');
+      47              : 
+      48              :   /// Returns the service id of the foreground service running in the
+      49              :   /// current isolate.
+      50              :   ///
+      51              :   /// Inside a [TaskHandler] this is set by the native side immediately
+      52              :   /// after the isolate starts (via the background method channel), and
+      53              :   /// reflects the service id passed to
+      54              :   /// [FlutterForegroundTaskController.startService].
+      55              :   ///
+      56              :   /// Outside of a task isolate (i.e. from the UI isolate), this always
+      57              :   /// returns `"default"` and should not be used.
+      58            1 :   static String get currentServiceId =>
+      59            1 :       FlutterForegroundTaskController.currentServiceId;
+      60              : 
+      61              :   // ====================== Service ======================
+      62              : 
+      63            2 :   @visibleForTesting
+      64              :   static AndroidNotificationOptions? get androidNotificationOptions =>
+      65            4 :       _default.androidNotificationOptions;
+      66              : 
+      67            1 :   @visibleForTesting
+      68              :   static set androidNotificationOptions(AndroidNotificationOptions? v) =>
+      69            2 :       _default.androidNotificationOptions = v;
+      70              : 
+      71            2 :   @visibleForTesting
+      72              :   static IOSNotificationOptions? get iosNotificationOptions =>
+      73            4 :       _default.iosNotificationOptions;
+      74              : 
+      75            1 :   @visibleForTesting
+      76              :   static set iosNotificationOptions(IOSNotificationOptions? v) =>
+      77            2 :       _default.iosNotificationOptions = v;
+      78              : 
+      79            2 :   @visibleForTesting
+      80              :   static ForegroundTaskOptions? get foregroundTaskOptions =>
+      81            4 :       _default.foregroundTaskOptions;
+      82              : 
+      83            1 :   @visibleForTesting
+      84              :   static set foregroundTaskOptions(ForegroundTaskOptions? v) =>
+      85            2 :       _default.foregroundTaskOptions = v;
+      86              : 
+      87            2 :   @visibleForTesting
+      88            4 :   static bool get isInitialized => _default.isInitialized;
+      89              : 
+      90            1 :   @visibleForTesting
+      91            2 :   static set isInitialized(bool v) => _default.isInitialized = v;
+      92              : 
+      93            1 :   @visibleForTesting
+      94              :   static bool get skipServiceResponseCheck =>
+      95            2 :       _default.skipServiceResponseCheck;
+      96              : 
+      97            2 :   @visibleForTesting
+      98              :   static set skipServiceResponseCheck(bool v) =>
+      99            4 :       _default.skipServiceResponseCheck = v;
+     100              : 
+     101              :   // platform instance: MethodChannelFlutterForegroundTask
+     102            2 :   static FlutterForegroundTaskPlatform get _platform =>
+     103            2 :       FlutterForegroundTaskPlatform.instance;
+     104              : 
+     105              :   /// Resets class's static values to allow for testing of service flow.
+     106            5 :   @visibleForTesting
+     107              :   static void resetStatic() {
+     108           10 :     _default.resetState();
+     109              :   }
+     110              : 
+     111              :   /// Initialize the [FlutterForegroundTask].
+     112            1 :   static void init({
+     113              :     required AndroidNotificationOptions androidNotificationOptions,
+     114              :     required IOSNotificationOptions iosNotificationOptions,
+     115              :     required ForegroundTaskOptions foregroundTaskOptions,
+     116              :   }) {
+     117            2 :     _default.init(
+     118              :       androidNotificationOptions: androidNotificationOptions,
+     119              :       iosNotificationOptions: iosNotificationOptions,
+     120              :       foregroundTaskOptions: foregroundTaskOptions,
+     121              :     );
+     122              :   }
+     123              : 
+     124              :   /// Start the foreground service.
+     125            1 :   static Future<ServiceRequestResult> startService({
+     126              :     int? serviceId,
+     127              :     List<ForegroundServiceTypes>? serviceTypes,
+     128              :     required String notificationTitle,
+     129              :     required String notificationText,
+     130              :     NotificationIcon? notificationIcon,
+     131              :     List<NotificationButton>? notificationButtons,
+     132              :     String? notificationInitialRoute,
+     133              :     Function? callback,
+     134              :   }) {
+     135            2 :     return _default.startService(
+     136              :       serviceId: serviceId,
+     137              :       serviceTypes: serviceTypes,
+     138              :       notificationTitle: notificationTitle,
+     139              :       notificationText: notificationText,
+     140              :       notificationIcon: notificationIcon,
+     141              :       notificationButtons: notificationButtons,
+     142              :       notificationInitialRoute: notificationInitialRoute,
+     143              :       callback: callback,
+     144              :     );
+     145              :   }
+     146              : 
+     147              :   /// Restart the foreground service.
+     148            1 :   static Future<ServiceRequestResult> restartService() {
+     149            2 :     return _default.restartService();
+     150              :   }
+     151              : 
+     152              :   /// Update the foreground service.
+     153            1 :   static Future<ServiceRequestResult> updateService({
+     154              :     ForegroundTaskOptions? foregroundTaskOptions,
+     155              :     String? notificationTitle,
+     156              :     String? notificationText,
+     157              :     NotificationIcon? notificationIcon,
+     158              :     List<NotificationButton>? notificationButtons,
+     159              :     String? notificationInitialRoute,
+     160              :     Function? callback,
+     161              :   }) {
+     162            2 :     return _default.updateService(
+     163              :       foregroundTaskOptions: foregroundTaskOptions,
+     164              :       notificationTitle: notificationTitle,
+     165              :       notificationText: notificationText,
+     166              :       notificationIcon: notificationIcon,
+     167              :       notificationButtons: notificationButtons,
+     168              :       notificationInitialRoute: notificationInitialRoute,
+     169              :       callback: callback,
+     170              :     );
+     171              :   }
+     172              : 
+     173              :   /// Stop the foreground service.
+     174            1 :   static Future<ServiceRequestResult> stopService() {
+     175            2 :     return _default.stopService();
+     176              :   }
+     177              : 
+     178            1 :   @visibleForTesting
+     179              :   static Future<void> checkServiceStateChange({required bool target}) {
+     180            2 :     return _default.checkServiceStateChange(target: target);
+     181              :   }
+     182              : 
+     183              :   /// Returns whether the foreground service is running.
+     184            6 :   static Future<bool> get isRunningService => _default.isRunningService;
+     185              : 
+     186              :   /// Set up the task handler and start the foreground task.
+     187              :   ///
+     188              :   /// It must always be called from a top-level function, otherwise foreground task will not work.
+     189            0 :   static void setTaskHandler(TaskHandler handler) =>
+     190            0 :       _platform.setTaskHandler(handler);
+     191              : 
+     192              :   // =================== Communication ===================
+     193              : 
+     194            2 :   @visibleForTesting
+     195            4 :   static ReceivePort? get receivePort => _default.receivePort;
+     196              : 
+     197            1 :   @visibleForTesting
+     198            2 :   static set receivePort(ReceivePort? v) => _default.receivePort = v;
+     199              : 
+     200            2 :   @visibleForTesting
+     201              :   static StreamSubscription? get streamSubscription =>
+     202            4 :       _default.streamSubscription;
+     203              : 
+     204            1 :   @visibleForTesting
+     205              :   static set streamSubscription(StreamSubscription? v) =>
+     206            2 :       _default.streamSubscription = v;
+     207              : 
+     208            1 :   @visibleForTesting
+     209            2 :   static List<DataCallback> get dataCallbacks => _default.dataCallbacks;
+     210              : 
+     211              :   /// Initialize port for communication between TaskHandler and UI.
+     212            2 :   static void initCommunicationPort() {
+     213            4 :     _default.initCommunicationPort();
+     214              :   }
+     215              : 
+     216              :   /// Add a callback to receive data sent from the [TaskHandler].
+     217            2 :   static void addTaskDataCallback(DataCallback callback) {
+     218            4 :     _default.addTaskDataCallback(callback);
+     219              :   }
+     220              : 
+     221              :   /// Remove a callback to receive data sent from the [TaskHandler].
+     222            1 :   static void removeTaskDataCallback(DataCallback callback) {
+     223            2 :     _default.removeTaskDataCallback(callback);
+     224              :   }
+     225              : 
+     226              :   /// Send data to [TaskHandler].
+     227            3 :   static void sendDataToTask(Object data) => _default.sendDataToTask(data);
+     228              : 
+     229              :   /// Send data to the main isolate.
+     230              :   ///
+     231              :   /// When called from inside a [TaskHandler] this automatically routes to
+     232              :   /// the controller whose service id matches the isolate's own service id,
+     233              :   /// so multi-service setups work without any extra plumbing.
+     234            1 :   static void sendDataToMain(Object data) {
+     235            3 :     FlutterForegroundTaskController.of(currentServiceId).sendDataToMain(data);
+     236              :   }
+     237              : 
+     238              :   // ====================== Storage ======================
+     239              : 
+     240              :   /// Get the stored data with [key].
+     241            1 :   static Future<T?> getData<T>({required String key}) =>
+     242            2 :       _default.getData<T>(key: key);
+     243              : 
+     244              :   /// Get all stored data.
+     245            3 :   static Future<Map<String, Object>> getAllData() => _default.getAllData();
+     246              : 
+     247              :   /// Save data with [key].
+     248            1 :   static Future<bool> saveData({
+     249              :     required String key,
+     250              :     required Object value,
+     251              :   }) =>
+     252            2 :       _default.saveData(key: key, value: value);
+     253              : 
+     254              :   /// Remove data with [key].
+     255            1 :   static Future<bool> removeData({required String key}) =>
+     256            2 :       _default.removeData(key: key);
+     257              : 
+     258              :   /// Clears all stored data.
+     259            3 :   static Future<bool> clearAllData() => _default.clearAllData();
+     260              : 
+     261              :   // ====================== Utility ======================
+     262              : 
+     263              :   /// Minimize the app to the background.
+     264            6 :   static void minimizeApp() => _platform.minimizeApp();
+     265              : 
+     266              :   /// Launch the app at [route] if it is not running otherwise open it.
+     267              :   ///
+     268              :   /// It is also possible to pass a route to this function but the route will only
+     269              :   /// be loaded if the app is not already running.
+     270              :   ///
+     271              :   /// This function requires the "android.permission.SYSTEM\_ALERT\_WINDOW" permission and
+     272              :   /// requires using the `openSystemAlertWindowSettings()` function to grant the permission.
+     273            3 :   static void launchApp([String? route]) => _platform.launchApp(route);
+     274              : 
+     275              :   /// Toggles lockScreen visibility.
+     276            1 :   static void setOnLockScreenVisibility(bool isVisible) =>
+     277            2 :       _platform.setOnLockScreenVisibility(isVisible);
+     278              : 
+     279              :   /// Returns whether the app is in the foreground.
+     280            3 :   static Future<bool> get isAppOnForeground => _platform.isAppOnForeground;
+     281              : 
+     282              :   /// Wake up the screen of a device that is turned off.
+     283            3 :   static void wakeUpScreen() => _platform.wakeUpScreen();
+     284              : 
+     285              :   /// Returns whether the app has been excluded from battery optimization.
+     286            1 :   static Future<bool> get isIgnoringBatteryOptimizations =>
+     287            2 :       _platform.isIgnoringBatteryOptimizations;
+     288              : 
+     289              :   /// Open the settings page where you can set ignore battery optimization.
+     290            1 :   static Future<bool> openIgnoreBatteryOptimizationSettings() =>
+     291            2 :       _platform.openIgnoreBatteryOptimizationSettings();
+     292              : 
+     293              :   /// Request to ignore battery optimization.
+     294              :   ///
+     295              :   /// This function requires the "android.permission.REQUEST\_IGNORE\_BATTERY\_OPTIMIZATIONS" permission.
+     296            1 :   static Future<bool> requestIgnoreBatteryOptimization() =>
+     297            2 :       _platform.requestIgnoreBatteryOptimization();
+     298              : 
+     299              :   /// Returns whether the "android.permission.SYSTEM\_ALERT\_WINDOW" permission is granted.
+     300            3 :   static Future<bool> get canDrawOverlays => _platform.canDrawOverlays;
+     301              : 
+     302              :   /// Open the settings page where you can allow/deny the "android.permission.SYSTEM\_ALERT\_WINDOW" permission.
+     303            1 :   static Future<bool> openSystemAlertWindowSettings() =>
+     304            2 :       _platform.openSystemAlertWindowSettings();
+     305              : 
+     306              :   /// Returns notification permission status.
+     307            1 :   static Future<NotificationPermission> checkNotificationPermission() =>
+     308            2 :       _platform.checkNotificationPermission();
+     309              : 
+     310              :   /// Request notification permission.
+     311            1 :   static Future<NotificationPermission> requestNotificationPermission() =>
+     312            2 :       _platform.requestNotificationPermission();
+     313              : 
+     314              :   /// Returns whether the "android.permission.SCHEDULE\_EXACT\_ALARM" permission is granted.
+     315            1 :   static Future<bool> get canScheduleExactAlarms =>
+     316            2 :       _platform.canScheduleExactAlarms;
+     317              : 
+     318              :   /// Open the alarms & reminders settings page.
+     319              :   ///
+     320              :   /// Use this utility only if you provide services that require long-term survival,
+     321              :   /// such as exact alarm service, healthcare service, or Bluetooth communication.
+     322              :   ///
+     323              :   /// This utility requires the "android.permission.SCHEDULE\_EXACT\_ALARM" permission.
+     324              :   /// Using this permission may make app distribution difficult due to Google policy.
+     325            1 :   static Future<bool> openAlarmsAndRemindersSettings() =>
+     326            2 :       _platform.openAlarmsAndRemindersSettings();
+     327              : 
+     328              :   // ======= iOS BGContinuedProcessingTask (iOS 26+) =======
+     329              : 
+     330              :   /// Updates the progress reported to the iOS `BGContinuedProcessingTask`
+     331              :   /// submitted when the service started.
+     332              :   ///
+     333              :   /// [progress] must be in the range `0.0` (just started) to `1.0` (done) and
+     334              :   /// is clamped automatically. Call this method at least once per reporting
+     335              :   /// interval -- the system will expire a continued processing task that stops
+     336              :   /// reporting progress.
+     337              :   ///
+     338              :   /// This is a no-op on Android and on iOS versions prior to 26, as well as
+     339              :   /// when the service was started without
+     340              :   /// [IOSContinuedProcessingTaskOptions].
+     341            1 :   static Future<void> updateIOSContinuedProcessingTaskProgress({
+     342              :     required double progress,
+     343              :   }) =>
+     344            2 :       _platform.updateIOSContinuedProcessingTaskProgress(progress: progress);
+     345              : 
+     346              :   /// Returns whether the current iOS version supports
+     347              :   /// `BGContinuedProcessingTask` (iOS 26+).
+     348              :   ///
+     349              :   /// Always returns `false` on Android and on iOS versions prior to 26.
+     350            1 :   static Future<bool> get isIOSContinuedProcessingTaskSupported =>
+     351            2 :       _platform.isIOSContinuedProcessingTaskSupported;
+     352              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/flutter_foreground_task_controller.dart.gcov.html b/coverage/html/lib/flutter_foreground_task_controller.dart.gcov.html new file mode 100644 index 00000000..30f779c9 --- /dev/null +++ b/coverage/html/lib/flutter_foreground_task_controller.dart.gcov.html @@ -0,0 +1,444 @@ + + + + + + + LCOV - lcov.info - lib/flutter_foreground_task_controller.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib - flutter_foreground_task_controller.dartCoverageTotalHit
Test:lcov.infoLines:98.4 %122120
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'dart:async';
+       2              : import 'dart:isolate';
+       3              : import 'dart:ui';
+       4              : 
+       5              : import 'package:shared_preferences/shared_preferences.dart';
+       6              : 
+       7              : import 'flutter_foreground_task_platform_interface.dart';
+       8              : import 'errors/service_already_started_exception.dart';
+       9              : import 'errors/service_not_initialized_exception.dart';
+      10              : import 'errors/service_not_started_exception.dart';
+      11              : import 'errors/service_timeout_exception.dart';
+      12              : import 'models/foreground_service_types.dart';
+      13              : import 'models/foreground_task_options.dart';
+      14              : import 'models/notification_button.dart';
+      15              : import 'models/notification_icon.dart';
+      16              : import 'models/notification_options.dart';
+      17              : import 'models/service_request_result.dart';
+      18              : import 'task_handler.dart';
+      19              : import 'utils/utility.dart';
+      20              : 
+      21              : typedef DataCallback = void Function(Object data);
+      22              : 
+      23              : /// A controller for managing a single foreground service instance.
+      24              : ///
+      25              : /// Use [FlutterForegroundTaskController.of] to obtain a controller for a
+      26              : /// given service id. The controller caches instances so the same id always
+      27              : /// returns the same controller.
+      28              : ///
+      29              : /// The `"default"` id corresponds to the built-in [ForegroundService] and
+      30              : /// is what [FlutterForegroundTask] uses under the hood for backward
+      31              : /// compatibility.
+      32              : class FlutterForegroundTaskController {
+      33            6 :   FlutterForegroundTaskController._(this.id);
+      34              : 
+      35              :   /// The service identifier this controller manages.
+      36              :   final String id;
+      37              : 
+      38           18 :   static final Map<String, FlutterForegroundTaskController> _instances = {};
+      39              : 
+      40              :   /// Returns the controller for the given [id], creating one if necessary.
+      41            6 :   factory FlutterForegroundTaskController.of(String id) =>
+      42           24 :       _instances.putIfAbsent(id, () => FlutterForegroundTaskController._(id));
+      43              : 
+      44              :   /// Returns the service id of the task isolate currently executing this
+      45              :   /// code, or `"default"` when called from the UI isolate / before the
+      46              :   /// service has finished starting.
+      47              :   ///
+      48              :   /// Set by the native side via the `onServiceIdSet` background channel
+      49              :   /// call once the [ForegroundTask] starts.
+      50            1 :   static String get currentServiceId => _currentServiceId;
+      51              :   static String _currentServiceId = 'default';
+      52              : 
+      53              :   /// Internal: set by [MethodChannelFlutterForegroundTask.onBackgroundChannel]
+      54              :   /// when the native side notifies this isolate of its service id.
+      55            1 :   static void setCurrentServiceId(String id) {
+      56              :     _currentServiceId = id;
+      57              :   }
+      58              : 
+      59              :   // platform instance
+      60            4 :   static FlutterForegroundTaskPlatform get _platform =>
+      61            4 :       FlutterForegroundTaskPlatform.instance;
+      62              : 
+      63              :   // ====================== State ======================
+      64              : 
+      65              :   AndroidNotificationOptions? androidNotificationOptions;
+      66              : 
+      67              :   IOSNotificationOptions? iosNotificationOptions;
+      68              : 
+      69              :   ForegroundTaskOptions? foregroundTaskOptions;
+      70              : 
+      71              :   bool isInitialized = false;
+      72              : 
+      73              :   bool skipServiceResponseCheck = false;
+      74              : 
+      75              :   /// Resets this controller's state to allow for testing of service flow.
+      76            5 :   void resetState() {
+      77            5 :     androidNotificationOptions = null;
+      78            5 :     iosNotificationOptions = null;
+      79            5 :     foregroundTaskOptions = null;
+      80            5 :     isInitialized = false;
+      81            5 :     skipServiceResponseCheck = false;
+      82              : 
+      83            7 :     receivePort?.close();
+      84            5 :     receivePort = null;
+      85            7 :     streamSubscription?.cancel();
+      86            5 :     streamSubscription = null;
+      87           10 :     dataCallbacks.clear();
+      88              : 
+      89              :     // `currentServiceId` is shared process-wide; reset it to the default
+      90              :     // so individual tests don't leak state across each other.
+      91              :     _currentServiceId = 'default';
+      92              :   }
+      93              : 
+      94              :   /// Initialize this controller's service configuration.
+      95            2 :   void init({
+      96              :     required AndroidNotificationOptions androidNotificationOptions,
+      97              :     required IOSNotificationOptions iosNotificationOptions,
+      98              :     required ForegroundTaskOptions foregroundTaskOptions,
+      99              :   }) {
+     100            2 :     this.androidNotificationOptions = androidNotificationOptions;
+     101            2 :     this.iosNotificationOptions = iosNotificationOptions;
+     102            2 :     this.foregroundTaskOptions = foregroundTaskOptions;
+     103            2 :     isInitialized = true;
+     104              :   }
+     105              : 
+     106              :   /// Start the foreground service.
+     107            1 :   Future<ServiceRequestResult> startService({
+     108              :     int? serviceId,
+     109              :     List<ForegroundServiceTypes>? serviceTypes,
+     110              :     required String notificationTitle,
+     111              :     required String notificationText,
+     112              :     NotificationIcon? notificationIcon,
+     113              :     List<NotificationButton>? notificationButtons,
+     114              :     String? notificationInitialRoute,
+     115              :     Function? callback,
+     116              :   }) async {
+     117              :     try {
+     118            1 :       if (!isInitialized) {
+     119            1 :         throw ServiceNotInitializedException();
+     120              :       }
+     121              : 
+     122            1 :       if (await isRunningService) {
+     123            1 :         throw ServiceAlreadyStartedException();
+     124              :       }
+     125              : 
+     126            2 :       await _platform.startService(
+     127            1 :         serviceId: id,
+     128            1 :         androidNotificationOptions: androidNotificationOptions!,
+     129            1 :         iosNotificationOptions: iosNotificationOptions!,
+     130            1 :         foregroundTaskOptions: foregroundTaskOptions!,
+     131              :         notificationId: serviceId,
+     132              :         serviceTypes: serviceTypes,
+     133              :         notificationTitle: notificationTitle,
+     134              :         notificationText: notificationText,
+     135              :         notificationIcon: notificationIcon,
+     136              :         notificationButtons: notificationButtons,
+     137              :         notificationInitialRoute: notificationInitialRoute,
+     138              :         callback: callback,
+     139              :       );
+     140              : 
+     141            1 :       if (!skipServiceResponseCheck) {
+     142            1 :         await checkServiceStateChange(target: true);
+     143              :       }
+     144              : 
+     145              :       return const ServiceRequestSuccess();
+     146              :     } catch (error) {
+     147            1 :       return ServiceRequestFailure(error: error);
+     148              :     }
+     149              :   }
+     150              : 
+     151              :   /// Restart the foreground service.
+     152            1 :   Future<ServiceRequestResult> restartService() async {
+     153              :     try {
+     154            1 :       if (!(await isRunningService)) {
+     155            1 :         throw ServiceNotStartedException();
+     156              :       }
+     157              : 
+     158            3 :       await _platform.restartService(serviceId: id);
+     159              : 
+     160              :       return const ServiceRequestSuccess();
+     161              :     } catch (error) {
+     162            1 :       return ServiceRequestFailure(error: error);
+     163              :     }
+     164              :   }
+     165              : 
+     166              :   /// Update the foreground service.
+     167            1 :   Future<ServiceRequestResult> updateService({
+     168              :     ForegroundTaskOptions? foregroundTaskOptions,
+     169              :     String? notificationTitle,
+     170              :     String? notificationText,
+     171              :     NotificationIcon? notificationIcon,
+     172              :     List<NotificationButton>? notificationButtons,
+     173              :     String? notificationInitialRoute,
+     174              :     Function? callback,
+     175              :   }) async {
+     176              :     try {
+     177            1 :       if (!(await isRunningService)) {
+     178            1 :         throw ServiceNotStartedException();
+     179              :       }
+     180              : 
+     181            2 :       await _platform.updateService(
+     182            1 :         serviceId: id,
+     183              :         foregroundTaskOptions: foregroundTaskOptions,
+     184              :         notificationText: notificationText,
+     185              :         notificationTitle: notificationTitle,
+     186              :         notificationIcon: notificationIcon,
+     187              :         notificationButtons: notificationButtons,
+     188              :         notificationInitialRoute: notificationInitialRoute,
+     189              :         callback: callback,
+     190              :       );
+     191              : 
+     192              :       return const ServiceRequestSuccess();
+     193              :     } catch (error) {
+     194            1 :       return ServiceRequestFailure(error: error);
+     195              :     }
+     196              :   }
+     197              : 
+     198              :   /// Stop the foreground service.
+     199            1 :   Future<ServiceRequestResult> stopService() async {
+     200              :     try {
+     201            1 :       if (!(await isRunningService)) {
+     202            1 :         throw ServiceNotStartedException();
+     203              :       }
+     204              : 
+     205            3 :       await _platform.stopService(serviceId: id);
+     206              : 
+     207            1 :       if (!skipServiceResponseCheck) {
+     208            1 :         await checkServiceStateChange(target: false);
+     209              :       }
+     210              : 
+     211              :       return const ServiceRequestSuccess();
+     212              :     } catch (error) {
+     213            1 :       return ServiceRequestFailure(error: error);
+     214              :     }
+     215              :   }
+     216              : 
+     217            2 :   Future<void> checkServiceStateChange({required bool target}) async {
+     218            4 :     final bool isCompleted = await Utility.instance.completedWithinDeadline(
+     219              :       deadline: const Duration(seconds: 5),
+     220            2 :       future: () async {
+     221            4 :         return target == await isRunningService;
+     222              :       },
+     223              :     );
+     224              : 
+     225              :     if (!isCompleted) {
+     226            1 :       throw ServiceTimeoutException();
+     227              :     }
+     228              :   }
+     229              : 
+     230              :   /// Returns whether this controller's foreground service is running.
+     231            3 :   Future<bool> get isRunningService =>
+     232            9 :       _platform.isRunningService(serviceId: id);
+     233              : 
+     234              :   /// Set up the task handler and start the foreground task.
+     235              :   ///
+     236              :   /// It must always be called from a top-level function, otherwise
+     237              :   /// foreground task will not work.
+     238            0 :   void setTaskHandler(TaskHandler handler) =>
+     239            0 :       _platform.setTaskHandler(handler);
+     240              : 
+     241              :   // =================== Communication ===================
+     242              : 
+     243              :   ReceivePort? receivePort;
+     244              : 
+     245              :   StreamSubscription? streamSubscription;
+     246              : 
+     247              :   final List<DataCallback> dataCallbacks = [];
+     248              : 
+     249            6 :   String get _portName => 'flutter_foreground_task/isolateComPort/$id';
+     250              : 
+     251              :   /// Initialize port for communication between TaskHandler and UI.
+     252            2 :   void initCommunicationPort() {
+     253            2 :     final ReceivePort newReceivePort = ReceivePort();
+     254            2 :     final SendPort newSendPort = newReceivePort.sendPort;
+     255              : 
+     256            4 :     IsolateNameServer.removePortNameMapping(_portName);
+     257            4 :     if (IsolateNameServer.registerPortWithName(newSendPort, _portName)) {
+     258            2 :       streamSubscription?.cancel();
+     259            2 :       receivePort?.close();
+     260              : 
+     261            2 :       receivePort = newReceivePort;
+     262            7 :       streamSubscription = receivePort?.listen((data) {
+     263            3 :         for (final DataCallback callback in dataCallbacks.toList()) {
+     264            1 :           callback.call(data);
+     265              :         }
+     266              :       });
+     267              :     }
+     268              :   }
+     269              : 
+     270              :   /// Add a callback to receive data sent from the [TaskHandler].
+     271            2 :   void addTaskDataCallback(DataCallback callback) {
+     272            4 :     if (!dataCallbacks.contains(callback)) {
+     273            4 :       dataCallbacks.add(callback);
+     274              :     }
+     275              :   }
+     276              : 
+     277              :   /// Remove a callback to receive data sent from the [TaskHandler].
+     278            1 :   void removeTaskDataCallback(DataCallback callback) {
+     279            2 :     dataCallbacks.remove(callback);
+     280              :   }
+     281              : 
+     282              :   /// Send data to [TaskHandler].
+     283            1 :   void sendDataToTask(Object data) =>
+     284            3 :       _platform.sendDataToTask(data, serviceId: id);
+     285              : 
+     286              :   /// Send data to main isolate.
+     287            1 :   void sendDataToMain(Object data) {
+     288              :     final SendPort? sendPort =
+     289            2 :         IsolateNameServer.lookupPortByName(_portName);
+     290            1 :     sendPort?.send(data);
+     291              :   }
+     292              : 
+     293              :   // ====================== Storage ======================
+     294              : 
+     295              :   static const String _kPrefsKeyPrefix =
+     296              :       'com.pravera.flutter_foreground_task.prefs.';
+     297              : 
+     298              :   /// Get the stored data with [key].
+     299            1 :   Future<T?> getData<T>({required String key}) async {
+     300            1 :     final SharedPreferences prefs = await SharedPreferences.getInstance();
+     301            1 :     await prefs.reload();
+     302              : 
+     303            2 :     final Object? data = prefs.get(_kPrefsKeyPrefix + key);
+     304              : 
+     305            1 :     return (data is T) ? data : null;
+     306              :   }
+     307              : 
+     308              :   /// Get all stored data.
+     309            1 :   Future<Map<String, Object>> getAllData() async {
+     310            1 :     final SharedPreferences prefs = await SharedPreferences.getInstance();
+     311            1 :     await prefs.reload();
+     312              : 
+     313            1 :     final Map<String, Object> dataList = {};
+     314            2 :     for (final String prefsKey in prefs.getKeys()) {
+     315            1 :       if (prefsKey.contains(_kPrefsKeyPrefix)) {
+     316            1 :         final Object? data = prefs.get(prefsKey);
+     317              :         if (data != null) {
+     318            1 :           final String originKey = prefsKey.replaceAll(_kPrefsKeyPrefix, '');
+     319            1 :           dataList[originKey] = data;
+     320              :         }
+     321              :       }
+     322              :     }
+     323              : 
+     324              :     return dataList;
+     325              :   }
+     326              : 
+     327              :   /// Save data with [key].
+     328            1 :   Future<bool> saveData({
+     329              :     required String key,
+     330              :     required Object value,
+     331              :   }) async {
+     332            1 :     final SharedPreferences prefs = await SharedPreferences.getInstance();
+     333            1 :     await prefs.reload();
+     334              : 
+     335            1 :     final String prefsKey = _kPrefsKeyPrefix + key;
+     336            1 :     if (value is int) {
+     337            1 :       return prefs.setInt(prefsKey, value);
+     338              :     } 
+     339            1 :     if (value is double) {
+     340            1 :       return prefs.setDouble(prefsKey, value);
+     341              :     }
+     342            1 :     if (value is String) {
+     343            1 :       return prefs.setString(prefsKey, value);
+     344              :     } 
+     345            1 :     if (value is bool) {
+     346            1 :       return prefs.setBool(prefsKey, value);
+     347              :     }
+     348              :     return false;
+     349              :   }
+     350              : 
+     351              :   /// Remove data with [key].
+     352            1 :   Future<bool> removeData({required String key}) async {
+     353            1 :     final SharedPreferences prefs = await SharedPreferences.getInstance();
+     354            1 :     await prefs.reload();
+     355              : 
+     356            2 :     return prefs.remove(_kPrefsKeyPrefix + key);
+     357              :   }
+     358              : 
+     359              :   /// Clears all stored data.
+     360            1 :   Future<bool> clearAllData() async {
+     361            1 :     final SharedPreferences prefs = await SharedPreferences.getInstance();
+     362            1 :     await prefs.reload();
+     363              : 
+     364            2 :     for (final String prefsKey in prefs.getKeys()) {
+     365            1 :       if (prefsKey.contains(_kPrefsKeyPrefix)) {
+     366            1 :         await prefs.remove(prefsKey);
+     367              :       }
+     368              :     }
+     369              : 
+     370              :     return true;
+     371              :   }
+     372              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/flutter_foreground_task_method_channel.dart.gcov.html b/coverage/html/lib/flutter_foreground_task_method_channel.dart.gcov.html new file mode 100644 index 00000000..bfdf607d --- /dev/null +++ b/coverage/html/lib/flutter_foreground_task_method_channel.dart.gcov.html @@ -0,0 +1,391 @@ + + + + + + + LCOV - lcov.info - lib/flutter_foreground_task_method_channel.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib - flutter_foreground_task_method_channel.dartCoverageTotalHit
Test:lcov.infoLines:93.3 %10598
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'dart:async';
+       2              : import 'dart:convert';
+       3              : import 'dart:developer' as dev;
+       4              : import 'dart:ui';
+       5              : 
+       6              : import 'package:flutter/services.dart';
+       7              : import 'package:flutter/widgets.dart';
+       8              : import 'package:platform/platform.dart';
+       9              : 
+      10              : import 'flutter_foreground_task_controller.dart';
+      11              : import 'flutter_foreground_task_platform_interface.dart';
+      12              : import 'models/foreground_service_types.dart';
+      13              : import 'models/foreground_task_options.dart';
+      14              : import 'models/notification_button.dart';
+      15              : import 'models/notification_icon.dart';
+      16              : import 'models/notification_options.dart';
+      17              : import 'models/notification_permission.dart';
+      18              : import 'models/service_options.dart';
+      19              : import 'task_handler.dart';
+      20              : 
+      21              : /// An implementation of [FlutterForegroundTaskPlatform] that uses method channels.
+      22              : class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform {
+      23              :   @visibleForTesting
+      24              :   final MethodChannel mMDChannel =
+      25              :       const MethodChannel('flutter_foreground_task/methods');
+      26              : 
+      27              :   @visibleForTesting
+      28              :   final MethodChannel mBGChannel =
+      29              :       const MethodChannel('flutter_foreground_task/background');
+      30              : 
+      31              :   @visibleForTesting
+      32              :   Platform platform = const LocalPlatform();
+      33              : 
+      34              :   // ====================== Service ======================
+      35              : 
+      36            1 :   @override
+      37              :   Future<void> startService({
+      38              :     required AndroidNotificationOptions androidNotificationOptions,
+      39              :     required IOSNotificationOptions iosNotificationOptions,
+      40              :     required ForegroundTaskOptions foregroundTaskOptions,
+      41              :     String serviceId = 'default',
+      42              :     int? notificationId,
+      43              :     List<ForegroundServiceTypes>? serviceTypes,
+      44              :     required String notificationTitle,
+      45              :     required String notificationText,
+      46              :     NotificationIcon? notificationIcon,
+      47              :     List<NotificationButton>? notificationButtons,
+      48              :     String? notificationInitialRoute,
+      49              :     Function? callback,
+      50              :   }) async {
+      51            1 :     final Map<String, dynamic> optionsJson = ServiceStartOptions(
+      52              :       serviceId: serviceId,
+      53              :       notificationId: notificationId,
+      54              :       serviceTypes: serviceTypes,
+      55              :       androidNotificationOptions: androidNotificationOptions,
+      56              :       iosNotificationOptions: iosNotificationOptions,
+      57              :       foregroundTaskOptions: foregroundTaskOptions,
+      58              :       notificationContentTitle: notificationTitle,
+      59              :       notificationContentText: notificationText,
+      60              :       notificationIcon: notificationIcon,
+      61              :       notificationButtons: notificationButtons,
+      62              :       notificationInitialRoute: notificationInitialRoute,
+      63              :       callback: callback,
+      64            2 :     ).toJson(platform);
+      65              : 
+      66            2 :     await mMDChannel.invokeMethod('startService', optionsJson);
+      67              :   }
+      68              : 
+      69            1 :   @override
+      70              :   Future<void> restartService({String serviceId = 'default'}) async {
+      71            3 :     await mMDChannel.invokeMethod('restartService', {'serviceId': serviceId});
+      72              :   }
+      73              : 
+      74            1 :   @override
+      75              :   Future<void> updateService({
+      76              :     String serviceId = 'default',
+      77              :     ForegroundTaskOptions? foregroundTaskOptions,
+      78              :     String? notificationTitle,
+      79              :     String? notificationText,
+      80              :     NotificationIcon? notificationIcon,
+      81              :     List<NotificationButton>? notificationButtons,
+      82              :     String? notificationInitialRoute,
+      83              :     Function? callback,
+      84              :   }) async {
+      85            1 :     final Map<String, dynamic> optionsJson = ServiceUpdateOptions(
+      86              :       serviceId: serviceId,
+      87              :       foregroundTaskOptions: foregroundTaskOptions,
+      88              :       notificationContentTitle: notificationTitle,
+      89              :       notificationContentText: notificationText,
+      90              :       notificationIcon: notificationIcon,
+      91              :       notificationButtons: notificationButtons,
+      92              :       notificationInitialRoute: notificationInitialRoute,
+      93              :       callback: callback,
+      94            2 :     ).toJson(platform);
+      95              : 
+      96            2 :     await mMDChannel.invokeMethod('updateService', optionsJson);
+      97              :   }
+      98              : 
+      99            1 :   @override
+     100              :   Future<void> stopService({String serviceId = 'default'}) async {
+     101            3 :     await mMDChannel.invokeMethod('stopService', {'serviceId': serviceId});
+     102              :   }
+     103              : 
+     104            3 :   @override
+     105              :   Future<bool> isRunningService({String serviceId = 'default'}) async {
+     106            3 :     return await mMDChannel
+     107            6 :         .invokeMethod('isRunningService', {'serviceId': serviceId});
+     108              :   }
+     109              : 
+     110            1 :   @override
+     111              :   Future<bool> get attachedActivity async {
+     112            2 :     if (platform.isAndroid) {
+     113            2 :       return await mMDChannel.invokeMethod('attachedActivity');
+     114              :     }
+     115              :     return true;
+     116              :   }
+     117              : 
+     118            0 :   @override
+     119              :   void setTaskHandler(TaskHandler handler) {
+     120              :     // Binding the framework to the flutter engine.
+     121            0 :     WidgetsFlutterBinding.ensureInitialized();
+     122            0 :     DartPluginRegistrant.ensureInitialized();
+     123              : 
+     124              :     // Set the method call handler for the background channel.
+     125            0 :     mBGChannel.setMethodCallHandler((call) async {
+     126            0 :       await onBackgroundChannel(call, handler);
+     127              :     });
+     128              : 
+     129            0 :     mBGChannel.invokeMethod('start');
+     130              :   }
+     131              : 
+     132            2 :   @visibleForTesting
+     133              :   Future<void> onBackgroundChannel(MethodCall call, TaskHandler handler) async {
+     134            2 :     final DateTime timestamp = DateTime.timestamp();
+     135              : 
+     136            2 :     switch (call.method) {
+     137            2 :       case 'onServiceIdSet':
+     138            1 :         final Object? arg = call.arguments;
+     139            1 :         if (arg is String) {
+     140            1 :           FlutterForegroundTaskController.setCurrentServiceId(arg);
+     141              :         }
+     142              :         break;
+     143            2 :       case 'onStart':
+     144            2 :         final TaskStarter starter = TaskStarter.fromIndex(call.arguments);
+     145            1 :         await handler.onStart(timestamp, starter);
+     146              :         break;
+     147            2 :       case 'onRepeatEvent':
+     148            1 :         handler.onRepeatEvent(timestamp);
+     149              :         break;
+     150            2 :       case 'onDestroy':
+     151            2 :         final bool isTimeout = call.arguments ?? false;
+     152            2 :         await handler.onDestroy(timestamp, isTimeout);
+     153              :         break;
+     154            1 :       case 'onReceiveData':
+     155            1 :         dynamic data = call.arguments;
+     156            3 :         if (data is List || data is Map || data is Set) {
+     157              :           try {
+     158            2 :             data = jsonDecode(jsonEncode(data));
+     159              :           } catch (e, s) {
+     160            0 :             dev.log('onReceiveData error: $e\n$s');
+     161              :           }
+     162              :         }
+     163            1 :         handler.onReceiveData(data);
+     164              :         break;
+     165            1 :       case 'onNotificationButtonPressed':
+     166            2 :         final String id = call.arguments.toString();
+     167            1 :         handler.onNotificationButtonPressed(id);
+     168              :         break;
+     169            1 :       case 'onNotificationDismissed':
+     170            1 :         handler.onNotificationDismissed();
+     171              :         break;
+     172            1 :       case 'onNotificationPressed':
+     173            1 :         handler.onNotificationPressed();
+     174              :         break;
+     175              :     }
+     176              :   }
+     177              : 
+     178              :   // =================== Communication ===================
+     179              : 
+     180            2 :   @override
+     181              :   void sendDataToTask(Object data, {String serviceId = 'default'}) {
+     182            6 :     mMDChannel.invokeMethod('sendData', {
+     183              :       'serviceId': serviceId,
+     184              :       'data': data,
+     185              :     });
+     186              :   }
+     187              : 
+     188              :   // ====================== Utility ======================
+     189              : 
+     190            2 :   @override
+     191              :   void minimizeApp() {
+     192            4 :     mMDChannel.invokeMethod('minimizeApp');
+     193              :   }
+     194              : 
+     195            1 :   @override
+     196              :   void launchApp([String? route]) {
+     197            2 :     if (platform.isAndroid) {
+     198            2 :       mMDChannel.invokeMethod('launchApp', route);
+     199              :     }
+     200              :   }
+     201              : 
+     202            1 :   @override
+     203              :   void setOnLockScreenVisibility(bool isVisible) {
+     204            2 :     if (platform.isAndroid) {
+     205            2 :       mMDChannel.invokeMethod('setOnLockScreenVisibility', isVisible);
+     206              :     }
+     207              :   }
+     208              : 
+     209            1 :   @override
+     210              :   Future<bool> get isAppOnForeground async {
+     211            2 :     return await mMDChannel.invokeMethod('isAppOnForeground');
+     212              :   }
+     213              : 
+     214            1 :   @override
+     215              :   void wakeUpScreen() {
+     216            2 :     if (platform.isAndroid) {
+     217            2 :       mMDChannel.invokeMethod('wakeUpScreen');
+     218              :     }
+     219              :   }
+     220              : 
+     221            1 :   @override
+     222              :   Future<bool> get isIgnoringBatteryOptimizations async {
+     223            2 :     if (platform.isAndroid) {
+     224            2 :       return await mMDChannel.invokeMethod('isIgnoringBatteryOptimizations');
+     225              :     }
+     226              :     return true;
+     227              :   }
+     228              : 
+     229            1 :   @override
+     230              :   Future<bool> openIgnoreBatteryOptimizationSettings() async {
+     231            2 :     if (platform.isAndroid) {
+     232            1 :       return await mMDChannel
+     233            1 :           .invokeMethod('openIgnoreBatteryOptimizationSettings');
+     234              :     }
+     235              :     return true;
+     236              :   }
+     237              : 
+     238            1 :   @override
+     239              :   Future<bool> requestIgnoreBatteryOptimization() async {
+     240            2 :     if (platform.isAndroid) {
+     241            2 :       return await mMDChannel.invokeMethod('requestIgnoreBatteryOptimization');
+     242              :     }
+     243              :     return true;
+     244              :   }
+     245              : 
+     246            1 :   @override
+     247              :   Future<bool> get canDrawOverlays async {
+     248            2 :     if (platform.isAndroid) {
+     249            2 :       return await mMDChannel.invokeMethod('canDrawOverlays');
+     250              :     }
+     251              :     return true;
+     252              :   }
+     253              : 
+     254            1 :   @override
+     255              :   Future<bool> openSystemAlertWindowSettings() async {
+     256            2 :     if (platform.isAndroid) {
+     257            2 :       return await mMDChannel.invokeMethod('openSystemAlertWindowSettings');
+     258              :     }
+     259              :     return true;
+     260              :   }
+     261              : 
+     262            1 :   @override
+     263              :   Future<NotificationPermission> checkNotificationPermission() async {
+     264              :     final int result =
+     265            2 :         await mMDChannel.invokeMethod('checkNotificationPermission');
+     266            1 :     return NotificationPermission.fromIndex(result);
+     267              :   }
+     268              : 
+     269            1 :   @override
+     270              :   Future<NotificationPermission> requestNotificationPermission() async {
+     271              :     final int result =
+     272            2 :         await mMDChannel.invokeMethod('requestNotificationPermission');
+     273            1 :     return NotificationPermission.fromIndex(result);
+     274              :   }
+     275              : 
+     276            1 :   @override
+     277              :   Future<bool> get canScheduleExactAlarms async {
+     278            2 :     if (platform.isAndroid) {
+     279            2 :       return await mMDChannel.invokeMethod('canScheduleExactAlarms');
+     280              :     }
+     281              :     return true;
+     282              :   }
+     283              : 
+     284            1 :   @override
+     285              :   Future<bool> openAlarmsAndRemindersSettings() async {
+     286            2 :     if (platform.isAndroid) {
+     287            2 :       return await mMDChannel.invokeMethod('openAlarmsAndRemindersSettings');
+     288              :     }
+     289              :     return true;
+     290              :   }
+     291              : 
+     292              :   // ======= iOS BGContinuedProcessingTask (iOS 26+) =======
+     293              : 
+     294            1 :   @override
+     295              :   Future<void> updateIOSContinuedProcessingTaskProgress({
+     296              :     required double progress,
+     297              :   }) async {
+     298            2 :     if (!platform.isIOS) {
+     299              :       return;
+     300              :     }
+     301            1 :     final double clamped = progress.isNaN
+     302              :         ? 0.0
+     303            2 :         : (progress < 0.0 ? 0.0 : (progress > 1.0 ? 1.0 : progress));
+     304            2 :     await mMDChannel.invokeMethod(
+     305              :       'updateIOSContinuedProcessingTaskProgress',
+     306            1 :       {'progress': clamped},
+     307              :     );
+     308              :   }
+     309              : 
+     310            1 :   @override
+     311              :   Future<bool> get isIOSContinuedProcessingTaskSupported async {
+     312            2 :     if (!platform.isIOS) {
+     313              :       return false;
+     314              :     }
+     315            1 :     return await mMDChannel
+     316            1 :             .invokeMethod<bool>('isIOSContinuedProcessingTaskSupported') ??
+     317              :         false;
+     318              :   }
+     319              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/flutter_foreground_task_platform_interface.dart.gcov.html b/coverage/html/lib/flutter_foreground_task_platform_interface.dart.gcov.html new file mode 100644 index 00000000..5293a786 --- /dev/null +++ b/coverage/html/lib/flutter_foreground_task_platform_interface.dart.gcov.html @@ -0,0 +1,244 @@ + + + + + + + LCOV - lcov.info - lib/flutter_foreground_task_platform_interface.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib - flutter_foreground_task_platform_interface.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %5555
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+       2              : 
+       3              : import 'flutter_foreground_task_method_channel.dart';
+       4              : import 'models/foreground_service_types.dart';
+       5              : import 'models/foreground_task_options.dart';
+       6              : import 'models/notification_button.dart';
+       7              : import 'models/notification_icon.dart';
+       8              : import 'models/notification_options.dart';
+       9              : import 'models/notification_permission.dart';
+      10              : import 'task_handler.dart';
+      11              : 
+      12              : abstract class FlutterForegroundTaskPlatform extends PlatformInterface {
+      13              :   /// Constructs a FlutterForegroundTaskPlatform.
+      14           18 :   FlutterForegroundTaskPlatform() : super(token: _token);
+      15              : 
+      16           18 :   static final Object _token = Object();
+      17              : 
+      18            7 :   static FlutterForegroundTaskPlatform _instance =
+      19            1 :       MethodChannelFlutterForegroundTask();
+      20              : 
+      21              :   /// The default instance of [FlutterForegroundTaskPlatform] to use.
+      22              :   ///
+      23              :   /// Defaults to [MethodChannelFlutterForegroundTask].
+      24           12 :   static FlutterForegroundTaskPlatform get instance => _instance;
+      25              : 
+      26              :   /// Platform-specific implementations should set this with their own
+      27              :   /// platform-specific class that extends [FlutterForegroundTaskPlatform] when
+      28              :   /// they register themselves.
+      29            6 :   static set instance(FlutterForegroundTaskPlatform instance) {
+      30           12 :     PlatformInterface.verifyToken(instance, _token);
+      31              :     _instance = instance;
+      32              :   }
+      33              : 
+      34              :   // ====================== Service ======================
+      35              : 
+      36            1 :   Future<void> startService({
+      37              :     required AndroidNotificationOptions androidNotificationOptions,
+      38              :     required IOSNotificationOptions iosNotificationOptions,
+      39              :     required ForegroundTaskOptions foregroundTaskOptions,
+      40              :     String serviceId = 'default',
+      41              :     int? notificationId,
+      42              :     List<ForegroundServiceTypes>? serviceTypes,
+      43              :     required String notificationTitle,
+      44              :     required String notificationText,
+      45              :     NotificationIcon? notificationIcon,
+      46              :     List<NotificationButton>? notificationButtons,
+      47              :     String? notificationInitialRoute,
+      48              :     Function? callback,
+      49              :   }) {
+      50            1 :     throw UnimplementedError('startService() has not been implemented.');
+      51              :   }
+      52              : 
+      53            1 :   Future<void> restartService({String serviceId = 'default'}) {
+      54            1 :     throw UnimplementedError('restartService() has not been implemented.');
+      55              :   }
+      56              : 
+      57            1 :   Future<void> updateService({
+      58              :     String serviceId = 'default',
+      59              :     ForegroundTaskOptions? foregroundTaskOptions,
+      60              :     String? notificationTitle,
+      61              :     String? notificationText,
+      62              :     NotificationIcon? notificationIcon,
+      63              :     List<NotificationButton>? notificationButtons,
+      64              :     String? notificationInitialRoute,
+      65              :     Function? callback,
+      66              :   }) {
+      67            1 :     throw UnimplementedError('updateService() has not been implemented.');
+      68              :   }
+      69              : 
+      70            1 :   Future<void> stopService({String serviceId = 'default'}) {
+      71            1 :     throw UnimplementedError('stopService() has not been implemented.');
+      72              :   }
+      73              : 
+      74            1 :   Future<bool> isRunningService({String serviceId = 'default'}) {
+      75            1 :     throw UnimplementedError('isRunningService has not been implemented.');
+      76              :   }
+      77              : 
+      78            1 :   Future<bool> get attachedActivity {
+      79            1 :     throw UnimplementedError('attachedActivity has not been implemented.');
+      80              :   }
+      81              : 
+      82            1 :   void setTaskHandler(TaskHandler handler) {
+      83            1 :     throw UnimplementedError('setTaskHandler() has not been implemented.');
+      84              :   }
+      85              : 
+      86              :   // =================== Communication ===================
+      87              : 
+      88            1 :   void sendDataToTask(Object data, {String serviceId = 'default'}) {
+      89            1 :     throw UnimplementedError('sendDataToTask() has not been implemented.');
+      90              :   }
+      91              : 
+      92              :   // ====================== Utility ======================
+      93              : 
+      94            1 :   void minimizeApp() {
+      95            1 :     throw UnimplementedError('minimizeApp() has not been implemented.');
+      96              :   }
+      97              : 
+      98            1 :   void launchApp([String? route]) {
+      99            1 :     throw UnimplementedError('launchApp() has not been implemented.');
+     100              :   }
+     101              : 
+     102            1 :   void setOnLockScreenVisibility(bool isVisible) {
+     103            1 :     throw UnimplementedError(
+     104              :         'setOnLockScreenVisibility() has not been implemented.');
+     105              :   }
+     106              : 
+     107            1 :   Future<bool> get isAppOnForeground {
+     108            1 :     throw UnimplementedError('isAppOnForeground has not been implemented.');
+     109              :   }
+     110              : 
+     111            1 :   void wakeUpScreen() {
+     112            1 :     throw UnimplementedError('wakeUpScreen() has not been implemented.');
+     113              :   }
+     114              : 
+     115            1 :   Future<bool> get isIgnoringBatteryOptimizations {
+     116            1 :     throw UnimplementedError(
+     117              :         'isIgnoringBatteryOptimizations has not been implemented.');
+     118              :   }
+     119              : 
+     120            1 :   Future<bool> openIgnoreBatteryOptimizationSettings() {
+     121            1 :     throw UnimplementedError(
+     122              :         'openIgnoreBatteryOptimizationSettings() has not been implemented.');
+     123              :   }
+     124              : 
+     125            1 :   Future<bool> requestIgnoreBatteryOptimization() {
+     126            1 :     throw UnimplementedError(
+     127              :         'requestIgnoreBatteryOptimization() has not been implemented.');
+     128              :   }
+     129              : 
+     130            1 :   Future<bool> get canDrawOverlays {
+     131            1 :     throw UnimplementedError('canDrawOverlays has not been implemented.');
+     132              :   }
+     133              : 
+     134            1 :   Future<bool> openSystemAlertWindowSettings() {
+     135            1 :     throw UnimplementedError(
+     136              :         'openSystemAlertWindowSettings() has not been implemented.');
+     137              :   }
+     138              : 
+     139            1 :   Future<NotificationPermission> checkNotificationPermission() {
+     140            1 :     throw UnimplementedError(
+     141              :         'checkNotificationPermission() has not been implemented.');
+     142              :   }
+     143              : 
+     144            1 :   Future<NotificationPermission> requestNotificationPermission() {
+     145            1 :     throw UnimplementedError(
+     146              :         'requestNotificationPermission() has not been implemented.');
+     147              :   }
+     148              : 
+     149            1 :   Future<bool> get canScheduleExactAlarms {
+     150            1 :     throw UnimplementedError(
+     151              :         'canScheduleExactAlarms has not been implemented.');
+     152              :   }
+     153              : 
+     154            1 :   Future<bool> openAlarmsAndRemindersSettings() {
+     155            1 :     throw UnimplementedError(
+     156              :         'openAlarmsAndRemindersSettings() has not been implemented.');
+     157              :   }
+     158              : 
+     159              :   // ======= iOS BGContinuedProcessingTask (iOS 26+) =======
+     160              : 
+     161            1 :   Future<void> updateIOSContinuedProcessingTaskProgress({
+     162              :     required double progress,
+     163              :   }) {
+     164            1 :     throw UnimplementedError(
+     165              :         'updateIOSContinuedProcessingTaskProgress() has not been implemented.');
+     166              :   }
+     167              : 
+     168            1 :   Future<bool> get isIOSContinuedProcessingTaskSupported {
+     169            1 :     throw UnimplementedError(
+     170              :         'isIOSContinuedProcessingTaskSupported has not been implemented.');
+     171              :   }
+     172              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/index-sort-l.html b/coverage/html/lib/index-sort-l.html new file mode 100644 index 00000000..acccac58 --- /dev/null +++ b/coverage/html/lib/index-sort-l.html @@ -0,0 +1,127 @@ + + + + + + + LCOV - lcov.info - lib + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - libCoverageTotalHit
Test:lcov.infoLines:97.1 %384373
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
flutter_foreground_task_method_channel.dart +
93.3%93.3%
+
93.3 %10598
flutter_foreground_task.dart +
97.9%97.9%
+
97.9 %9795
flutter_foreground_task_controller.dart +
98.4%98.4%
+
98.4 %122120
task_handler.dart +
100.0%
+
100.0 %55
flutter_foreground_task_platform_interface.dart +
100.0%
+
100.0 %5555
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/index.html b/coverage/html/lib/index.html new file mode 100644 index 00000000..d831db67 --- /dev/null +++ b/coverage/html/lib/index.html @@ -0,0 +1,127 @@ + + + + + + + LCOV - lcov.info - lib + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - libCoverageTotalHit
Test:lcov.infoLines:97.1 %384373
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
flutter_foreground_task.dart +
97.9%97.9%
+
97.9 %9795
flutter_foreground_task_controller.dart +
98.4%98.4%
+
98.4 %122120
flutter_foreground_task_method_channel.dart +
93.3%93.3%
+
93.3 %10598
flutter_foreground_task_platform_interface.dart +
100.0%
+
100.0 %5555
task_handler.dart +
100.0%
+
100.0 %55
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/foreground_service_types.dart.gcov.html b/coverage/html/lib/models/foreground_service_types.dart.gcov.html new file mode 100644 index 00000000..1c1bfa2d --- /dev/null +++ b/coverage/html/lib/models/foreground_service_types.dart.gcov.html @@ -0,0 +1,129 @@ + + + + + + + LCOV - lcov.info - lib/models/foreground_service_types.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - foreground_service_types.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %11
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /// https://developer.android.com/about/versions/14/changes/fgs-types-required#system-exempted
+       2              : class ForegroundServiceTypes {
+       3              :   /// Constructs an instance of [ForegroundServiceTypes].
+       4            3 :   const ForegroundServiceTypes(this.rawValue);
+       5              : 
+       6              :   /// Continue to access the camera from the background, such as video chat apps that allow for multitasking.
+       7              :   static const camera = ForegroundServiceTypes(0);
+       8              : 
+       9              :   /// Interactions with external devices that require a Bluetooth, NFC, IR, USB, or network connection.
+      10              :   static const connectedDevice = ForegroundServiceTypes(1);
+      11              : 
+      12              :   /// Data transfer operations, such as the following:
+      13              :   ///
+      14              :   /// * Data upload or download
+      15              :   /// * Backup-and-restore operations
+      16              :   /// * Import or export operations
+      17              :   /// * Fetch data
+      18              :   /// * Local file processing
+      19              :   /// * Transfer data between a device and the cloud over a network
+      20              :   static const dataSync = ForegroundServiceTypes(2);
+      21              : 
+      22              :   /// Any long-running use cases to support apps in the fitness category such as exercise trackers.
+      23              :   static const health = ForegroundServiceTypes(3);
+      24              : 
+      25              :   /// Long-running use cases that require location access, such as navigation and location sharing.
+      26              :   static const location = ForegroundServiceTypes(4);
+      27              : 
+      28              :   /// Continue audio or video playback from the background. Support Digital Video Recording (DVR) functionality on Android TV.
+      29              :   static const mediaPlayback = ForegroundServiceTypes(5);
+      30              : 
+      31              :   /// Project content to non-primary display or external device using the MediaProjection APIs. This content doesn't have to be exclusively media content.
+      32              :   static const mediaProjection = ForegroundServiceTypes(6);
+      33              : 
+      34              :   /// Continue microphone capture from the background, such as voice recorders or communication apps.
+      35              :   static const microphone = ForegroundServiceTypes(7);
+      36              : 
+      37              :   /// Continue an ongoing call using the ConnectionService APIs.
+      38              :   static const phoneCall = ForegroundServiceTypes(8);
+      39              : 
+      40              :   /// Transfer text messages from one device to another. Assists with continuity of a user's messaging tasks when they switch devices.
+      41              :   static const remoteMessaging = ForegroundServiceTypes(9);
+      42              : 
+      43              :   /// Quickly finish critical work that cannot be interrupted or postponed.
+      44              :   static const shortService = ForegroundServiceTypes(10);
+      45              : 
+      46              :   /// Covers any valid foreground service use cases that aren't covered by the other foreground service types.
+      47              :   static const specialUse = ForegroundServiceTypes(11);
+      48              : 
+      49              :   /// Reserved for system applications and specific system integrations, to continue to use foreground services.
+      50              :   static const systemExempted = ForegroundServiceTypes(12);
+      51              : 
+      52              :   /// Service for performing time-consuming operations on media assets, like converting media to different formats.
+      53              :   static const mediaProcessing = ForegroundServiceTypes(13);
+      54              : 
+      55              :   /// The raw value of [ForegroundServiceTypes].
+      56              :   final int rawValue;
+      57              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/foreground_task_event_action.dart.gcov.html b/coverage/html/lib/models/foreground_task_event_action.dart.gcov.html new file mode 100644 index 00000000..3a2f4256 --- /dev/null +++ b/coverage/html/lib/models/foreground_task_event_action.dart.gcov.html @@ -0,0 +1,122 @@ + + + + + + + LCOV - lcov.info - lib/models/foreground_task_event_action.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - foreground_task_event_action.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %1111
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /// A class that defines the action of onRepeatEvent in [TaskHandler].
+       2              : class ForegroundTaskEventAction {
+       3            4 :   ForegroundTaskEventAction._private({
+       4              :     required this.type,
+       5              :     this.interval,
+       6              :   });
+       7              : 
+       8              :   /// Not use onRepeatEvent callback.
+       9            3 :   factory ForegroundTaskEventAction.nothing() =>
+      10            3 :       ForegroundTaskEventAction._private(type: ForegroundTaskEventType.nothing);
+      11              : 
+      12              :   /// Call onRepeatEvent only once.
+      13            1 :   factory ForegroundTaskEventAction.once() =>
+      14            1 :       ForegroundTaskEventAction._private(type: ForegroundTaskEventType.once);
+      15              : 
+      16              :   /// Call onRepeatEvent at milliseconds [interval].
+      17            2 :   factory ForegroundTaskEventAction.repeat(int interval) =>
+      18            2 :       ForegroundTaskEventAction._private(
+      19              :           type: ForegroundTaskEventType.repeat, interval: interval);
+      20              : 
+      21              :   /// The type for [ForegroundTaskEventAction].
+      22              :   final ForegroundTaskEventType type;
+      23              : 
+      24              :   /// The interval(in milliseconds) at which onRepeatEvent is invoked.
+      25              :   final int? interval;
+      26              : 
+      27              :   /// Returns the data fields of [ForegroundTaskEventAction] in JSON format.
+      28            2 :   Map<String, dynamic> toJson() {
+      29            2 :     return {
+      30            4 :       'taskEventType': type.value,
+      31            2 :       'taskEventInterval': interval,
+      32              :     };
+      33              :   }
+      34              : }
+      35              : 
+      36              : /// The type for [ForegroundTaskEventAction].
+      37              : enum ForegroundTaskEventType {
+      38              :   /// Not use onRepeatEvent callback.
+      39              :   nothing(1),
+      40              : 
+      41              :   /// Call onRepeatEvent only once.
+      42              :   once(2),
+      43              : 
+      44              :   /// Call onRepeatEvent at milliseconds interval.
+      45              :   repeat(3);
+      46              : 
+      47              :   const ForegroundTaskEventType(this.value);
+      48              : 
+      49              :   final int value;
+      50              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/foreground_task_options.dart.gcov.html b/coverage/html/lib/models/foreground_task_options.dart.gcov.html new file mode 100644 index 00000000..c1b221d4 --- /dev/null +++ b/coverage/html/lib/models/foreground_task_options.dart.gcov.html @@ -0,0 +1,152 @@ + + + + + + + LCOV - lcov.info - lib/models/foreground_task_options.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - foreground_task_options.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %1919
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'foreground_task_event_action.dart';
+       2              : 
+       3              : /// Data class with foreground task options.
+       4              : class ForegroundTaskOptions {
+       5              :   /// Constructs an instance of [ForegroundTaskOptions].
+       6            4 :   const ForegroundTaskOptions({
+       7              :     required this.eventAction,
+       8              :     this.autoRunOnBoot = false,
+       9              :     this.autoRunOnMyPackageReplaced = false,
+      10              :     this.allowWakeLock = true,
+      11              :     this.allowWifiLock = false,
+      12              :     this.allowAutoRestart = true,
+      13              :     this.stopWithTask,
+      14              :   });
+      15              : 
+      16              :   /// The action of onRepeatEvent in [TaskHandler].
+      17              :   final ForegroundTaskEventAction eventAction;
+      18              : 
+      19              :   /// Whether to automatically run foreground task on boot.
+      20              :   /// The default is `false`.
+      21              :   final bool autoRunOnBoot;
+      22              : 
+      23              :   /// Whether to automatically run foreground task when the app is updated to a new version.
+      24              :   /// The default is `false`.
+      25              :   final bool autoRunOnMyPackageReplaced;
+      26              : 
+      27              :   /// Whether to keep the CPU turned on.
+      28              :   /// The default is `true`.
+      29              :   final bool allowWakeLock;
+      30              : 
+      31              :   /// Allows an application to keep the Wi-Fi radio awake.
+      32              :   /// The default is `false`.
+      33              :   ///
+      34              :   /// https://developer.android.com/reference/android/net/wifi/WifiManager.WifiLock.html
+      35              :   final bool allowWifiLock;
+      36              : 
+      37              :   /// Allows an application to automatically restart when the app is killed by the system.
+      38              :   ///
+      39              :   /// https://developer.android.com/about/versions/15/behavior-changes-15?hl=pt-br#datasync-timeout
+      40              :   final bool allowAutoRestart;
+      41              : 
+      42              :   /// Allows an application to automatically stop when the app task is removed by the system.
+      43              :   /// If set, overrides the service android:stopWithTask behavior.
+      44              :   final bool? stopWithTask;
+      45              : 
+      46              :   /// Returns the data fields of [ForegroundTaskOptions] in JSON format.
+      47            2 :   Map<String, dynamic> toJson() {
+      48            2 :     return {
+      49            6 :       'taskEventAction': eventAction.toJson(),
+      50            4 :       'autoRunOnBoot': autoRunOnBoot,
+      51            4 :       'autoRunOnMyPackageReplaced': autoRunOnMyPackageReplaced,
+      52            4 :       'allowWakeLock': allowWakeLock,
+      53            4 :       'allowWifiLock': allowWifiLock,
+      54            4 :       'allowAutoRestart': allowAutoRestart,
+      55            4 :       if (stopWithTask != null) 'stopWithTask': stopWithTask,
+      56              :     };
+      57              :   }
+      58              : 
+      59              :   static const _unset = Object();
+      60              : 
+      61              :   /// Creates a copy of the object replaced with new values.
+      62            1 :   ForegroundTaskOptions copyWith({
+      63              :     ForegroundTaskEventAction? eventAction,
+      64              :     bool? autoRunOnBoot,
+      65              :     bool? autoRunOnMyPackageReplaced,
+      66              :     bool? allowWakeLock,
+      67              :     bool? allowWifiLock,
+      68              :     bool? allowAutoRestart,
+      69              :     Object? stopWithTask = _unset,
+      70              :   }) =>
+      71            1 :       ForegroundTaskOptions(
+      72            1 :         eventAction: eventAction ?? this.eventAction,
+      73            1 :         autoRunOnBoot: autoRunOnBoot ?? this.autoRunOnBoot,
+      74            1 :         autoRunOnMyPackageReplaced: autoRunOnMyPackageReplaced ?? this.autoRunOnMyPackageReplaced,
+      75            1 :         allowWakeLock: allowWakeLock ?? this.allowWakeLock,
+      76            1 :         allowWifiLock: allowWifiLock ?? this.allowWifiLock,
+      77            1 :         allowAutoRestart: allowAutoRestart ?? this.allowAutoRestart,
+      78            1 :         stopWithTask: identical(stopWithTask, _unset) ? this.stopWithTask : stopWithTask as bool?,
+      79              :       );
+      80              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/index-sort-l.html b/coverage/html/lib/models/index-sort-l.html new file mode 100644 index 00000000..4fed8e0d --- /dev/null +++ b/coverage/html/lib/models/index-sort-l.html @@ -0,0 +1,190 @@ + + + + + + + LCOV - lcov.info - lib/models + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/modelsCoverageTotalHit
Test:lcov.infoLines:99.4 %157156
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
notification_options.dart +
98.4%98.4%
+
98.4 %6160
foreground_service_types.dart +
100.0%
+
100.0 %11
notification_channel_importance.dart +
100.0%
+
100.0 %11
notification_priority.dart +
100.0%
+
100.0 %11
notification_visibility.dart +
100.0%
+
100.0 %11
notification_permission.dart +
100.0%
+
100.0 %22
service_request_result.dart +
100.0%
+
100.0 %33
notification_icon.dart +
100.0%
+
100.0 %1010
foreground_task_event_action.dart +
100.0%
+
100.0 %1111
notification_button.dart +
100.0%
+
100.0 %1313
foreground_task_options.dart +
100.0%
+
100.0 %1919
service_options.dart +
100.0%
+
100.0 %3434
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/index.html b/coverage/html/lib/models/index.html new file mode 100644 index 00000000..fd65da3b --- /dev/null +++ b/coverage/html/lib/models/index.html @@ -0,0 +1,190 @@ + + + + + + + LCOV - lcov.info - lib/models + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/modelsCoverageTotalHit
Test:lcov.infoLines:99.4 %157156
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
foreground_service_types.dart +
100.0%
+
100.0 %11
foreground_task_event_action.dart +
100.0%
+
100.0 %1111
foreground_task_options.dart +
100.0%
+
100.0 %1919
notification_button.dart +
100.0%
+
100.0 %1313
notification_channel_importance.dart +
100.0%
+
100.0 %11
notification_icon.dart +
100.0%
+
100.0 %1010
notification_options.dart +
98.4%98.4%
+
98.4 %6160
notification_permission.dart +
100.0%
+
100.0 %22
notification_priority.dart +
100.0%
+
100.0 %11
notification_visibility.dart +
100.0%
+
100.0 %11
service_options.dart +
100.0%
+
100.0 %3434
service_request_result.dart +
100.0%
+
100.0 %33
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/notification_button.dart.gcov.html b/coverage/html/lib/models/notification_button.dart.gcov.html new file mode 100644 index 00000000..ec2a0a3e --- /dev/null +++ b/coverage/html/lib/models/notification_button.dart.gcov.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - lcov.info - lib/models/notification_button.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - notification_button.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %1313
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'dart:ui';
+       2              : 
+       3              : import 'package:flutter_foreground_task/utils/color_extension.dart';
+       4              : 
+       5              : /// The button to display in the notification.
+       6              : class NotificationButton {
+       7              :   /// Constructs an instance of [NotificationButton].
+       8            1 :   const NotificationButton({
+       9              :     required this.id,
+      10              :     required this.text,
+      11              :     this.textColor,
+      12            3 :   })  : assert(id.length > 0),
+      13            3 :         assert(text.length > 0);
+      14              : 
+      15              :   /// The button identifier.
+      16              :   final String id;
+      17              : 
+      18              :   /// The text to display on the button.
+      19              :   final String text;
+      20              : 
+      21              :   /// The button text color. (only work Android)
+      22              :   final Color? textColor;
+      23              : 
+      24              :   /// Returns the data fields of [NotificationButton] in JSON format.
+      25            2 :   Map<String, dynamic> toJson() {
+      26            2 :     return {
+      27            2 :       'id': id,
+      28            2 :       'text': text,
+      29            4 :       'textColorRgb': textColor?.toRgbString,
+      30              :     };
+      31              :   }
+      32              : 
+      33              :   /// Creates a copy of the object replaced with new values.
+      34            1 :   NotificationButton copyWith({
+      35              :     String? id,
+      36              :     String? text,
+      37              :     Color? textColor,
+      38              :   }) =>
+      39            1 :       NotificationButton(
+      40            1 :         id: id ?? this.id,
+      41            1 :         text: text ?? this.text,
+      42            1 :         textColor: textColor ?? this.textColor,
+      43              :       );
+      44              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/notification_channel_importance.dart.gcov.html b/coverage/html/lib/models/notification_channel_importance.dart.gcov.html new file mode 100644 index 00000000..2d64192d --- /dev/null +++ b/coverage/html/lib/models/notification_channel_importance.dart.gcov.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - lib/models/notification_channel_importance.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - notification_channel_importance.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %11
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /// The importance of the notification channel.
+       2              : /// See https://developer.android.com/training/notify-user/channels?hl=ko#importance
+       3              : class NotificationChannelImportance {
+       4              :   /// Constructs an instance of [NotificationChannelImportance].
+       5            3 :   const NotificationChannelImportance(this.rawValue);
+       6              : 
+       7              :   /// A notification with no importance: does not show in the shade.
+       8              :   static const NotificationChannelImportance NONE =
+       9              :       NotificationChannelImportance(0);
+      10              : 
+      11              :   /// Min notification importance: only shows in the shade, below the fold.
+      12              :   static const NotificationChannelImportance MIN =
+      13              :       NotificationChannelImportance(1);
+      14              : 
+      15              :   /// Low notification importance: shows in the shade, and potentially in the status bar (see shouldHideSilentStatusBarIcons()), but is not audibly intrusive.
+      16              :   static const NotificationChannelImportance LOW =
+      17              :       NotificationChannelImportance(2);
+      18              : 
+      19              :   /// Default notification importance: shows everywhere, makes noise, but does not visually intrude.
+      20              :   static const NotificationChannelImportance DEFAULT =
+      21              :       NotificationChannelImportance(3);
+      22              : 
+      23              :   /// Higher notification importance: shows everywhere, makes noise and peeks. May use full screen intents.
+      24              :   static const NotificationChannelImportance HIGH =
+      25              :       NotificationChannelImportance(4);
+      26              : 
+      27              :   /// Max notification importance: same as HIGH, but generally not used.
+      28              :   static const NotificationChannelImportance MAX =
+      29              :       NotificationChannelImportance(5);
+      30              : 
+      31              :   /// The raw value of [NotificationChannelImportance].
+      32              :   final int rawValue;
+      33              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/notification_icon.dart.gcov.html b/coverage/html/lib/models/notification_icon.dart.gcov.html new file mode 100644 index 00000000..543c13b9 --- /dev/null +++ b/coverage/html/lib/models/notification_icon.dart.gcov.html @@ -0,0 +1,108 @@ + + + + + + + LCOV - lcov.info - lib/models/notification_icon.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - notification_icon.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %1010
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'dart:ui';
+       2              : 
+       3              : import 'package:flutter_foreground_task/utils/color_extension.dart';
+       4              : 
+       5              : /// A data class for dynamically changing the notification icon.
+       6              : class NotificationIcon {
+       7              :   /// Constructs an instance of [NotificationIcon].
+       8            1 :   const NotificationIcon({
+       9              :     required this.metaDataName,
+      10              :     this.backgroundColor,
+      11            3 :   }) : assert(metaDataName.length > 0);
+      12              : 
+      13              :   /// The name of the meta-data in the manifest that contains the drawable icon resource identifier.
+      14              :   final String metaDataName;
+      15              : 
+      16              :   /// The background color for the notification icon.
+      17              :   final Color? backgroundColor;
+      18              : 
+      19              :   /// Returns the data fields of [NotificationIcon] in JSON format.
+      20            2 :   Map<String, dynamic> toJson() {
+      21            2 :     return {
+      22            2 :       'metaDataName': metaDataName,
+      23            4 :       'backgroundColorRgb': backgroundColor?.toRgbString,
+      24              :     };
+      25              :   }
+      26              : 
+      27              :   /// Creates a copy of the object replaced with new values.
+      28            1 :   NotificationIcon copyWith({
+      29              :     String? metaDataName,
+      30              :     Color? backgroundColor,
+      31              :   }) =>
+      32            1 :       NotificationIcon(
+      33            1 :         metaDataName: metaDataName ?? this.metaDataName,
+      34            1 :         backgroundColor: backgroundColor ?? this.backgroundColor,
+      35              :       );
+      36              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/notification_options.dart.gcov.html b/coverage/html/lib/models/notification_options.dart.gcov.html new file mode 100644 index 00000000..af82e4a3 --- /dev/null +++ b/coverage/html/lib/models/notification_options.dart.gcov.html @@ -0,0 +1,379 @@ + + + + + + + LCOV - lcov.info - lib/models/notification_options.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - notification_options.dartCoverageTotalHit
Test:lcov.infoLines:98.4 %6160
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'notification_channel_importance.dart';
+       2              : import 'notification_priority.dart';
+       3              : import 'notification_visibility.dart';
+       4              : 
+       5              : /// Notification options for Android platform.
+       6              : class AndroidNotificationOptions {
+       7              :   /// Constructs an instance of [AndroidNotificationOptions].
+       8            4 :   AndroidNotificationOptions({
+       9              :     @Deprecated('Use startService(serviceId) instead.') this.id,
+      10              :     required this.channelId,
+      11              :     required this.channelName,
+      12              :     this.channelDescription,
+      13              :     this.channelImportance = NotificationChannelImportance.LOW,
+      14              :     this.priority = NotificationPriority.LOW,
+      15              :     this.enableVibration = false,
+      16              :     this.playSound = false,
+      17              :     this.showWhen = false,
+      18              :     this.showBadge = false,
+      19              :     this.onlyAlertOnce = false,
+      20              :     this.visibility = NotificationVisibility.VISIBILITY_PUBLIC,
+      21              :     this.storagePrefix,
+      22            8 :   })  : assert(channelId.isNotEmpty),
+      23            8 :         assert(channelName.isNotEmpty);
+      24              : 
+      25              :   /// Unique ID of the notification.
+      26              :   final int? id;
+      27              : 
+      28              :   /// Unique ID of the notification channel.
+      29              :   ///
+      30              :   /// It is set only once for the first time on Android 8.0+.
+      31              :   final String channelId;
+      32              : 
+      33              :   /// The name of the notification channel.
+      34              :   ///
+      35              :   /// It is set only once for the first time on Android 8.0+.
+      36              :   final String channelName;
+      37              : 
+      38              :   /// The description of the notification channel.
+      39              :   ///
+      40              :   /// It is set only once for the first time on Android 8.0+.
+      41              :   final String? channelDescription;
+      42              : 
+      43              :   /// The importance of the notification channel.
+      44              :   /// The default is `NotificationChannelImportance.LOW`.
+      45              :   ///
+      46              :   /// It is set only once for the first time on Android 8.0+.
+      47              :   final NotificationChannelImportance channelImportance;
+      48              : 
+      49              :   /// Priority of notifications for Android 7.1 and lower.
+      50              :   /// The default is `NotificationPriority.LOW`.
+      51              :   final NotificationPriority priority;
+      52              : 
+      53              :   /// Whether to enable vibration when creating notifications.
+      54              :   /// The default is `false`.
+      55              :   ///
+      56              :   /// It is set only once for the first time on Android 8.0+.
+      57              :   final bool enableVibration;
+      58              : 
+      59              :   /// Whether to play sound when creating notifications.
+      60              :   /// The default is `false`.
+      61              :   ///
+      62              :   /// It is set only once for the first time on Android 8.0+.
+      63              :   final bool playSound;
+      64              : 
+      65              :   /// Whether to show the timestamp when the notification was created in the content view.
+      66              :   /// The default is `false`.
+      67              :   final bool showWhen;
+      68              : 
+      69              :   /// Whether to show the badge near the app icon when service is started.
+      70              :   /// The default is `false`.
+      71              :   ///
+      72              :   /// It is set only once for the first time on Android 8.0+.
+      73              :   final bool showBadge;
+      74              : 
+      75              :   /// Whether to only alert once when the notification is created.
+      76              :   /// The default is `false`.
+      77              :   final bool onlyAlertOnce;
+      78              : 
+      79              :   /// Control the level of detail displayed in notifications on the lock screen.
+      80              :   /// The default is `NotificationVisibility.VISIBILITY_PUBLIC`.
+      81              :   final NotificationVisibility visibility;
+      82              : 
+      83              :   /// Custom prefix for Android `SharedPreferences` file names.
+      84              :   ///
+      85              :   /// By default the library uses named preference files prefixed with
+      86              :   /// `com.pravera.flutter_foreground_task.prefs.` that are fully isolated
+      87              :   /// from the host app's default preferences. Override this when you need
+      88              :   /// a specific prefix, for example to share state with another component.
+      89              :   ///
+      90              :   /// Passing `null` (the default) keeps the library's own prefix.
+      91              :   final String? storagePrefix;
+      92              : 
+      93              :   /// Returns the data fields of [AndroidNotificationOptions] in JSON format.
+      94            2 :   Map<String, dynamic> toJson() {
+      95            2 :     return {
+      96            4 :       'notificationId': id,
+      97            4 :       'notificationChannelId': channelId,
+      98            4 :       'notificationChannelName': channelName,
+      99            4 :       'notificationChannelDescription': channelDescription,
+     100            6 :       'notificationChannelImportance': channelImportance.rawValue,
+     101            6 :       'notificationPriority': priority.rawValue,
+     102            4 :       'enableVibration': enableVibration,
+     103            4 :       'playSound': playSound,
+     104            4 :       'showWhen': showWhen,
+     105            4 :       'showBadge': showBadge,
+     106            4 :       'onlyAlertOnce': onlyAlertOnce,
+     107            6 :       'visibility': visibility.rawValue,
+     108            4 :       if (storagePrefix != null) 'storagePrefix': storagePrefix,
+     109              :     };
+     110              :   }
+     111              : 
+     112              :   /// Creates a copy of the object replaced with new values.
+     113            1 :   AndroidNotificationOptions copyWith({
+     114              :     String? channelId,
+     115              :     String? channelName,
+     116              :     String? channelDescription,
+     117              :     NotificationChannelImportance? channelImportance,
+     118              :     NotificationPriority? priority,
+     119              :     bool? enableVibration,
+     120              :     bool? playSound,
+     121              :     bool? showWhen,
+     122              :     bool? showBadge,
+     123              :     bool? onlyAlertOnce,
+     124              :     NotificationVisibility? visibility,
+     125              :     String? storagePrefix,
+     126              :   }) =>
+     127            1 :       AndroidNotificationOptions(
+     128            1 :         channelId: channelId ?? this.channelId,
+     129            1 :         channelName: channelName ?? this.channelName,
+     130            1 :         channelDescription: channelDescription ?? this.channelDescription,
+     131            1 :         channelImportance: channelImportance ?? this.channelImportance,
+     132            1 :         priority: priority ?? this.priority,
+     133            1 :         enableVibration: enableVibration ?? this.enableVibration,
+     134            1 :         playSound: playSound ?? this.playSound,
+     135            1 :         showWhen: showWhen ?? this.showWhen,
+     136            1 :         showBadge: showBadge ?? this.showBadge,
+     137            1 :         onlyAlertOnce: onlyAlertOnce ?? this.onlyAlertOnce,
+     138            1 :         visibility: visibility ?? this.visibility,
+     139            1 :         storagePrefix: storagePrefix ?? this.storagePrefix,
+     140              :       );
+     141              : }
+     142              : 
+     143              : /// Notification options for iOS platform.
+     144              : class IOSNotificationOptions {
+     145              :   /// Constructs an instance of [IOSNotificationOptions].
+     146            2 :   const IOSNotificationOptions({
+     147              :     this.showNotification = true,
+     148              :     this.playSound = false,
+     149              :     this.continuedProcessingTask,
+     150              :     this.storageSuiteName,
+     151              :   });
+     152              : 
+     153              :   /// Whether to show notifications.
+     154              :   /// The default is `true`.
+     155              :   final bool showNotification;
+     156              : 
+     157              :   /// Whether to play sound when creating notifications.
+     158              :   /// The default is `false`.
+     159              :   final bool playSound;
+     160              : 
+     161              :   /// Options for submitting an iOS 26+ `BGContinuedProcessingTask` alongside
+     162              :   /// the service. When non-null and the device runs iOS 26 or later, starting
+     163              :   /// the service submits a continued processing task so the system can manage
+     164              :   /// progress UI and continue the work if the user backgrounds the app.
+     165              :   ///
+     166              :   /// On older iOS versions this value is ignored — the service uses the
+     167              :   /// legacy background execution model.
+     168              :   ///
+     169              :   /// IMPORTANT: Apple requires the continued processing task to be submitted
+     170              :   /// in direct response to a user action. `startService` must therefore be
+     171              :   /// invoked synchronously from an explicit user gesture (e.g. a button tap).
+     172              :   final IOSContinuedProcessingTaskOptions? continuedProcessingTask;
+     173              : 
+     174              :   /// Custom `UserDefaults` suite name for iOS-side persistence.
+     175              :   ///
+     176              :   /// By default the library uses a namespaced suite
+     177              :   /// (`com.pravera.flutter_foreground_task`) that is fully isolated from
+     178              :   /// the host app's `UserDefaults.standard`. Override this when you need a
+     179              :   /// specific suite, for example an App Group suite for sharing state with
+     180              :   /// an extension.
+     181              :   ///
+     182              :   /// Passing `null` (the default) keeps the library's own isolated suite.
+     183              :   final String? storageSuiteName;
+     184              : 
+     185              :   /// Returns the data fields of [IOSNotificationOptions] in JSON format.
+     186            3 :   Map<String, dynamic> toJson() {
+     187            3 :     return {
+     188            6 :       'showNotification': showNotification,
+     189            6 :       'playSound': playSound,
+     190            3 :       if (continuedProcessingTask != null)
+     191            3 :         'continuedProcessingTask': continuedProcessingTask!.toJson(),
+     192            5 :       if (storageSuiteName != null) 'storageSuiteName': storageSuiteName,
+     193              :     };
+     194              :   }
+     195              : 
+     196              :   /// Creates a copy of the object replaced with new values.
+     197            1 :   IOSNotificationOptions copyWith({
+     198              :     bool? showNotification,
+     199              :     bool? playSound,
+     200              :     IOSContinuedProcessingTaskOptions? continuedProcessingTask,
+     201              :     String? storageSuiteName,
+     202              :   }) =>
+     203            1 :       IOSNotificationOptions(
+     204            1 :         showNotification: showNotification ?? this.showNotification,
+     205            1 :         playSound: playSound ?? this.playSound,
+     206              :         continuedProcessingTask:
+     207            1 :             continuedProcessingTask ?? this.continuedProcessingTask,
+     208            1 :         storageSuiteName: storageSuiteName ?? this.storageSuiteName,
+     209              :       );
+     210              : }
+     211              : 
+     212              : /// Submission strategy for a `BGContinuedProcessingTaskRequest`.
+     213              : ///
+     214              : /// Mirrors `BGContinuedProcessingTaskRequest.SubmissionStrategy` introduced
+     215              : /// in iOS 26.
+     216              : enum IOSContinuedProcessingSubmissionStrategy {
+     217              :   /// Let the system queue the request if it can't run immediately.
+     218              :   /// This is the system default and is appropriate when the user can tolerate
+     219              :   /// a short delay before the work begins.
+     220              :   queue('queue'),
+     221              : 
+     222              :   /// Fail the submission immediately if the system cannot start the task
+     223              :   /// right away. Use this when the user is actively waiting and a deferred
+     224              :   /// start would be worse than an error.
+     225              :   fail('fail');
+     226              : 
+     227              :   const IOSContinuedProcessingSubmissionStrategy(this.rawValue);
+     228              : 
+     229              :   /// The raw string value sent across the method channel to the native layer.
+     230              :   final String rawValue;
+     231              : }
+     232              : 
+     233              : /// Options for an iOS 26+ `BGContinuedProcessingTask`.
+     234              : ///
+     235              : /// When provided on [IOSNotificationOptions.continuedProcessingTask], starting
+     236              : /// the foreground service also submits a `BGContinuedProcessingTaskRequest`
+     237              : /// so the system:
+     238              : ///
+     239              : /// - Shows a user-facing progress UI with a cancel affordance.
+     240              : /// - Continues executing the task when the app is backgrounded.
+     241              : /// - Reclaims execution time based on the progress you report rather than a
+     242              : ///   hard time limit.
+     243              : ///
+     244              : /// Setup checklist:
+     245              : ///
+     246              : /// 1. Enable the *Background processing* capability in Xcode.
+     247              : /// 2. Add your `identifier` to `BGTaskSchedulerPermittedIdentifiers` in your
+     248              : ///    app's `Info.plist`. A wildcard suffix (e.g. `com.example.app.upload.*`)
+     249              : ///    is supported if you need per-invocation identifiers.
+     250              : /// 3. Call [FlutterForegroundTask.updateIOSContinuedProcessingTaskProgress]
+     251              : ///    from your task handler at least once per reporting interval. The system
+     252              : ///    will expire a task that stops reporting progress.
+     253              : /// 4. Stop the service (or let the task handler finish) to complete the task.
+     254              : ///
+     255              : /// See: https://developer.apple.com/documentation/backgroundtasks/performing-long-running-tasks-on-ios-and-ipados
+     256              : class IOSContinuedProcessingTaskOptions {
+     257              :   /// Constructs an instance of [IOSContinuedProcessingTaskOptions].
+     258            2 :   const IOSContinuedProcessingTaskOptions({
+     259              :     required this.identifier,
+     260              :     required this.title,
+     261              :     this.subtitle,
+     262              :     this.submissionStrategy =
+     263              :         IOSContinuedProcessingSubmissionStrategy.queue,
+     264            6 :   }) : assert(identifier.length > 0),
+     265            6 :         assert(title.length > 0);
+     266              : 
+     267              :   /// The `BGContinuedProcessingTaskRequest` identifier.
+     268              :   ///
+     269              :   /// Must exactly match one of the entries (or wildcard patterns) declared in
+     270              :   /// `BGTaskSchedulerPermittedIdentifiers` in the app's `Info.plist`.
+     271              :   final String identifier;
+     272              : 
+     273              :   /// Localized title shown in the system progress UI while the task runs.
+     274              :   final String title;
+     275              : 
+     276              :   /// Optional localized subtitle shown beneath the title in the system
+     277              :   /// progress UI.
+     278              :   final String? subtitle;
+     279              : 
+     280              :   /// Submission strategy controlling how the system treats the request when
+     281              :   /// it cannot start the task immediately.
+     282              :   final IOSContinuedProcessingSubmissionStrategy submissionStrategy;
+     283              : 
+     284              :   /// Returns the data fields in JSON format for the method channel.
+     285            1 :   Map<String, dynamic> toJson() {
+     286            1 :     return {
+     287            1 :       'identifier': identifier,
+     288            1 :       'title': title,
+     289            1 :       'subtitle': subtitle,
+     290            2 :       'submissionStrategy': submissionStrategy.rawValue,
+     291              :     };
+     292              :   }
+     293              : 
+     294              :   /// Creates a copy of the object replaced with new values.
+     295            1 :   IOSContinuedProcessingTaskOptions copyWith({
+     296              :     String? identifier,
+     297              :     String? title,
+     298              :     String? subtitle,
+     299              :     IOSContinuedProcessingSubmissionStrategy? submissionStrategy,
+     300              :   }) =>
+     301            1 :       IOSContinuedProcessingTaskOptions(
+     302            1 :         identifier: identifier ?? this.identifier,
+     303            0 :         title: title ?? this.title,
+     304            1 :         subtitle: subtitle ?? this.subtitle,
+     305            1 :         submissionStrategy: submissionStrategy ?? this.submissionStrategy,
+     306              :       );
+     307              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/notification_permission.dart.gcov.html b/coverage/html/lib/models/notification_permission.dart.gcov.html new file mode 100644 index 00000000..fa353837 --- /dev/null +++ b/coverage/html/lib/models/notification_permission.dart.gcov.html @@ -0,0 +1,86 @@ + + + + + + + LCOV - lcov.info - lib/models/notification_permission.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - notification_permission.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %22
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /// Represents the result of a notification permission request.
+       2              : enum NotificationPermission {
+       3              :   /// Notification permission has been granted.
+       4              :   granted,
+       5              : 
+       6              :   /// Notification permission has been denied.
+       7              :   denied,
+       8              : 
+       9              :   /// Notification permission has been permanently denied.
+      10              :   permanently_denied;
+      11              : 
+      12            2 :   static NotificationPermission fromIndex(int? index) => NotificationPermission
+      13            2 :       .values[index ?? NotificationPermission.denied.index];
+      14              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/notification_priority.dart.gcov.html b/coverage/html/lib/models/notification_priority.dart.gcov.html new file mode 100644 index 00000000..ef028813 --- /dev/null +++ b/coverage/html/lib/models/notification_priority.dart.gcov.html @@ -0,0 +1,95 @@ + + + + + + + LCOV - lcov.info - lib/models/notification_priority.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - notification_priority.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %11
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /// Priority of notifications for Android 7.1 and lower.
+       2              : class NotificationPriority {
+       3              :   /// Constructs an instance of [NotificationPriority].
+       4            3 :   const NotificationPriority(this.rawValue);
+       5              : 
+       6              :   /// No sound and does not appear in the status bar.
+       7              :   static const NotificationPriority MIN = NotificationPriority(-2);
+       8              : 
+       9              :   /// No sound.
+      10              :   static const NotificationPriority LOW = NotificationPriority(-1);
+      11              : 
+      12              :   /// Makes a sound.
+      13              :   static const NotificationPriority DEFAULT = NotificationPriority(0);
+      14              : 
+      15              :   /// Makes a sound and appears as a heads-up notification.
+      16              :   static const NotificationPriority HIGH = NotificationPriority(1);
+      17              : 
+      18              :   /// Same as HIGH, but used when you want to notify notification immediately.
+      19              :   static const NotificationPriority MAX = NotificationPriority(2);
+      20              : 
+      21              :   /// The raw value of [NotificationPriority].
+      22              :   final int rawValue;
+      23              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/notification_visibility.dart.gcov.html b/coverage/html/lib/models/notification_visibility.dart.gcov.html new file mode 100644 index 00000000..dccfbc76 --- /dev/null +++ b/coverage/html/lib/models/notification_visibility.dart.gcov.html @@ -0,0 +1,92 @@ + + + + + + + LCOV - lcov.info - lib/models/notification_visibility.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - notification_visibility.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %11
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /// The level of detail displayed in notifications on the lock screen.
+       2              : class NotificationVisibility {
+       3              :   /// Constructs an instance of [NotificationVisibility].
+       4            3 :   const NotificationVisibility(this.rawValue);
+       5              : 
+       6              :   /// Show this notification in its entirety on all lockscreens.
+       7              :   static const NotificationVisibility VISIBILITY_PUBLIC =
+       8              :       NotificationVisibility(1);
+       9              : 
+      10              :   /// Do not reveal any part of this notification on a secure lockscreen.
+      11              :   static const NotificationVisibility VISIBILITY_SECRET =
+      12              :       NotificationVisibility(-1);
+      13              : 
+      14              :   /// Show this notification on all lockscreens, but conceal sensitive or private information on secure lockscreens.
+      15              :   static const NotificationVisibility VISIBILITY_PRIVATE =
+      16              :       NotificationVisibility(0);
+      17              : 
+      18              :   /// The raw value of [NotificationVisibility].
+      19              :   final int rawValue;
+      20              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/service_options.dart.gcov.html b/coverage/html/lib/models/service_options.dart.gcov.html new file mode 100644 index 00000000..e14ca7fe --- /dev/null +++ b/coverage/html/lib/models/service_options.dart.gcov.html @@ -0,0 +1,185 @@ + + + + + + + LCOV - lcov.info - lib/models/service_options.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - service_options.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %3434
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'dart:ui';
+       2              : 
+       3              : import 'package:platform/platform.dart';
+       4              : 
+       5              : import 'foreground_service_types.dart';
+       6              : import 'foreground_task_options.dart';
+       7              : import 'notification_button.dart';
+       8              : import 'notification_icon.dart';
+       9              : import 'notification_options.dart';
+      10              : 
+      11              : class ServiceStartOptions {
+      12            1 :   const ServiceStartOptions({
+      13              :     this.serviceId = 'default',
+      14              :     this.notificationId,
+      15              :     this.serviceTypes,
+      16              :     required this.androidNotificationOptions,
+      17              :     required this.iosNotificationOptions,
+      18              :     required this.foregroundTaskOptions,
+      19              :     required this.notificationContentTitle,
+      20              :     required this.notificationContentText,
+      21              :     this.notificationIcon,
+      22              :     this.notificationButtons,
+      23              :     this.notificationInitialRoute,
+      24              :     this.callback,
+      25              :   });
+      26              : 
+      27              :   final String serviceId;
+      28              :   final int? notificationId;
+      29              :   final List<ForegroundServiceTypes>? serviceTypes;
+      30              :   final AndroidNotificationOptions androidNotificationOptions;
+      31              :   final IOSNotificationOptions iosNotificationOptions;
+      32              :   final ForegroundTaskOptions foregroundTaskOptions;
+      33              :   final String notificationContentTitle;
+      34              :   final String notificationContentText;
+      35              :   final NotificationIcon? notificationIcon;
+      36              :   final List<NotificationButton>? notificationButtons;
+      37              :   final String? notificationInitialRoute;
+      38              :   final Function? callback;
+      39              : 
+      40            1 :   Map<String, dynamic> toJson(Platform platform) {
+      41            1 :     final Map<String, dynamic> json = {
+      42            2 :       'serviceId': serviceId,
+      43            2 :       'serviceTypes': serviceTypes?.map((e) => e.rawValue).toList(),
+      44            2 :       ...foregroundTaskOptions.toJson(),
+      45            2 :       'notificationContentTitle': notificationContentTitle,
+      46            2 :       'notificationContentText': notificationContentText,
+      47            3 :       'icon': notificationIcon?.toJson(),
+      48            6 :       'buttons': notificationButtons?.map((e) => e.toJson()).toList(),
+      49            2 :       'initialRoute': notificationInitialRoute,
+      50              :     };
+      51              : 
+      52            1 :     if (notificationId != null) {
+      53            2 :       json['notificationId'] = notificationId;
+      54              :     }
+      55              : 
+      56            1 :     if (platform.isAndroid) {
+      57            3 :       json.addAll(androidNotificationOptions.toJson());
+      58            1 :     } else if (platform.isIOS) {
+      59            3 :       json.addAll(iosNotificationOptions.toJson());
+      60              :     }
+      61              : 
+      62            1 :     if (callback != null) {
+      63            1 :       json['callbackHandle'] =
+      64            3 :           PluginUtilities.getCallbackHandle(callback!)?.toRawHandle();
+      65              :     }
+      66              : 
+      67              :     return json;
+      68              :   }
+      69              : }
+      70              : 
+      71              : class ServiceUpdateOptions {
+      72            1 :   const ServiceUpdateOptions({
+      73              :     this.serviceId = 'default',
+      74              :     required this.foregroundTaskOptions,
+      75              :     required this.notificationContentTitle,
+      76              :     required this.notificationContentText,
+      77              :     this.notificationIcon,
+      78              :     this.notificationButtons,
+      79              :     this.notificationInitialRoute,
+      80              :     this.callback,
+      81              :   });
+      82              : 
+      83              :   final String serviceId;
+      84              :   final ForegroundTaskOptions? foregroundTaskOptions;
+      85              :   final String? notificationContentTitle;
+      86              :   final String? notificationContentText;
+      87              :   final NotificationIcon? notificationIcon;
+      88              :   final List<NotificationButton>? notificationButtons;
+      89              :   final String? notificationInitialRoute;
+      90              :   final Function? callback;
+      91              : 
+      92            1 :   Map<String, dynamic> toJson(Platform platform) {
+      93            1 :     final Map<String, dynamic> json = {
+      94            1 :       'serviceId': serviceId,
+      95            1 :       'notificationContentTitle': notificationContentTitle,
+      96            1 :       'notificationContentText': notificationContentText,
+      97            2 :       'icon': notificationIcon?.toJson(),
+      98            5 :       'buttons': notificationButtons?.map((e) => e.toJson()).toList(),
+      99            1 :       'initialRoute': notificationInitialRoute,
+     100              :     };
+     101              : 
+     102            1 :     if (foregroundTaskOptions != null) {
+     103            3 :       json.addAll(foregroundTaskOptions!.toJson());
+     104              :     }
+     105              : 
+     106            1 :     if (callback != null) {
+     107            1 :       json['callbackHandle'] =
+     108            3 :           PluginUtilities.getCallbackHandle(callback!)?.toRawHandle();
+     109              :     }
+     110              : 
+     111              :     return json;
+     112              :   }
+     113              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/models/service_request_result.dart.gcov.html b/coverage/html/lib/models/service_request_result.dart.gcov.html new file mode 100644 index 00000000..6c11e37b --- /dev/null +++ b/coverage/html/lib/models/service_request_result.dart.gcov.html @@ -0,0 +1,89 @@ + + + + + + + LCOV - lcov.info - lib/models/service_request_result.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/models - service_request_result.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %33
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /// Represents the result of a service request.
+       2              : sealed class ServiceRequestResult {
+       3            4 :   const ServiceRequestResult();
+       4              : }
+       5              : 
+       6              : /// The service request was successful.
+       7              : final class ServiceRequestSuccess extends ServiceRequestResult {
+       8            3 :   const ServiceRequestSuccess();
+       9              : }
+      10              : 
+      11              : /// The service request failed.
+      12              : final class ServiceRequestFailure extends ServiceRequestResult {
+      13            2 :   const ServiceRequestFailure({required this.error});
+      14              : 
+      15              :   /// The error that occurred when the service request failed.
+      16              :   final Object error;
+      17              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/task_handler.dart.gcov.html b/coverage/html/lib/task_handler.dart.gcov.html new file mode 100644 index 00000000..9b3a62e7 --- /dev/null +++ b/coverage/html/lib/task_handler.dart.gcov.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - lcov.info - lib/task_handler.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib - task_handler.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %55
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'flutter_foreground_task.dart';
+       2              : 
+       3              : /// A class that implements a task handler.
+       4              : abstract class TaskHandler {
+       5              :   /// Called when the task is started.
+       6              :   Future<void> onStart(DateTime timestamp, TaskStarter starter);
+       7              : 
+       8              :   /// Called based on the eventAction set in [ForegroundTaskOptions].
+       9              :   ///
+      10              :   /// - .nothing() : Not use onRepeatEvent callback.
+      11              :   /// - .once() : Call onRepeatEvent only once.
+      12              :   /// - .repeat(interval) : Call onRepeatEvent at milliseconds interval.
+      13              :   void onRepeatEvent(DateTime timestamp);
+      14              : 
+      15              :   /// Called when the task is destroyed.
+      16              :   Future<void> onDestroy(DateTime timestamp, bool isTimeout);
+      17              : 
+      18              :   /// Called when data is sent using [FlutterForegroundTask.sendDataToTask].
+      19            1 :   void onReceiveData(Object data) {}
+      20              : 
+      21              :   /// Called when the notification button is pressed.
+      22            1 :   void onNotificationButtonPressed(String id) {}
+      23              : 
+      24              :   /// Called when the notification itself is pressed.
+      25            1 :   void onNotificationPressed() {}
+      26              : 
+      27              :   /// Called when the notification itself is dismissed.
+      28              :   ///
+      29              :   /// - AOS: only work Android 14+
+      30              :   ///
+      31              :   /// - iOS: only work iOS 12+
+      32            1 :   void onNotificationDismissed() {}
+      33              : }
+      34              : 
+      35              : /// The starter that started the task.
+      36              : enum TaskStarter {
+      37              :   /// The task has been started by the developer.
+      38              :   developer,
+      39              : 
+      40              :   /// The task has been started by the system.
+      41              :   system;
+      42              : 
+      43            4 :   static TaskStarter fromIndex(int index) => TaskStarter.values[index];
+      44              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/ui/index.html b/coverage/html/lib/ui/index.html new file mode 100644 index 00000000..4c50d7d1 --- /dev/null +++ b/coverage/html/lib/ui/index.html @@ -0,0 +1,91 @@ + + + + + + + LCOV - lcov.info - lib/ui + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/uiCoverageTotalHit
Test:lcov.infoLines:100.0 %1212
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
with_foreground_task.dart +
100.0%
+
100.0 %1212
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/ui/with_foreground_task.dart.gcov.html b/coverage/html/lib/ui/with_foreground_task.dart.gcov.html new file mode 100644 index 00000000..50463133 --- /dev/null +++ b/coverage/html/lib/ui/with_foreground_task.dart.gcov.html @@ -0,0 +1,110 @@ + + + + + + + LCOV - lcov.info - lib/ui/with_foreground_task.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/ui - with_foreground_task.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %1212
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'package:flutter/material.dart';
+       2              : import 'package:flutter/services.dart';
+       3              : import 'package:flutter_foreground_task/flutter_foreground_task.dart';
+       4              : 
+       5              : /// A widget that minimizes the app without closing it when the user presses
+       6              : /// the back button while the foreground service is running.
+       7              : ///
+       8              : /// This widget must be declared above the [Scaffold] widget.
+       9              : class WithForegroundTask extends StatefulWidget {
+      10              :   /// A child widget that contains the [Scaffold] widget.
+      11              :   final Widget child;
+      12              : 
+      13            1 :   const WithForegroundTask({super.key, required this.child});
+      14              : 
+      15            1 :   @override
+      16            1 :   State<StatefulWidget> createState() => _WithForegroundTaskState();
+      17              : }
+      18              : 
+      19              : class _WithForegroundTaskState extends State<WithForegroundTask> {
+      20            1 :   Future<void> _onPopInvokedWithResult(bool didPop, dynamic result) async {
+      21              :     if (didPop) return;
+      22              : 
+      23            1 :     if (await FlutterForegroundTask.isRunningService) {
+      24            1 :       FlutterForegroundTask.minimizeApp();
+      25              :     } else {
+      26            1 :       SystemNavigator.pop();
+      27              :     }
+      28              :   }
+      29              : 
+      30            1 :   @override
+      31              :   Widget build(BuildContext context) {
+      32            1 :     return PopScope(
+      33            1 :       canPop: Navigator.canPop(context),
+      34            1 :       onPopInvokedWithResult: _onPopInvokedWithResult,
+      35            2 :       child: widget.child,
+      36              :     );
+      37              :   }
+      38              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/utils/color_extension.dart.gcov.html b/coverage/html/lib/utils/color_extension.dart.gcov.html new file mode 100644 index 00000000..1b9d3eff --- /dev/null +++ b/coverage/html/lib/utils/color_extension.dart.gcov.html @@ -0,0 +1,80 @@ + + + + + + + LCOV - lcov.info - lib/utils/color_extension.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/utils - color_extension.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %44
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import 'dart:ui';
+       2              : 
+       3              : extension ColorExtension on Color {
+       4            4 :   String get toRgbString =>
+       5            8 :       '${(r * 255.0).round().clamp(0, 255)},'
+       6            8 :       '${(g * 255.0).round().clamp(0, 255)},'
+       7            8 :       '${(b * 255.0).round().clamp(0, 255)}';
+       8              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/utils/index-sort-l.html b/coverage/html/lib/utils/index-sort-l.html new file mode 100644 index 00000000..b5b19fa4 --- /dev/null +++ b/coverage/html/lib/utils/index-sort-l.html @@ -0,0 +1,100 @@ + + + + + + + LCOV - lcov.info - lib/utils + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/utilsCoverageTotalHit
Test:lcov.infoLines:100.0 %1212
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
color_extension.dart +
100.0%
+
100.0 %44
utility.dart +
100.0%
+
100.0 %88
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/utils/index.html b/coverage/html/lib/utils/index.html new file mode 100644 index 00000000..2ea5393e --- /dev/null +++ b/coverage/html/lib/utils/index.html @@ -0,0 +1,100 @@ + + + + + + + LCOV - lcov.info - lib/utils + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/utilsCoverageTotalHit
Test:lcov.infoLines:100.0 %1212
Test Date:2026-04-17 21:56:09
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
color_extension.dart +
100.0%
+
100.0 %44
utility.dart +
100.0%
+
100.0 %88
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/lib/utils/utility.dart.gcov.html b/coverage/html/lib/utils/utility.dart.gcov.html new file mode 100644 index 00000000..b944affc --- /dev/null +++ b/coverage/html/lib/utils/utility.dart.gcov.html @@ -0,0 +1,98 @@ + + + + + + + LCOV - lcov.info - lib/utils/utility.dart + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - lib/utils - utility.dartCoverageTotalHit
Test:lcov.infoLines:100.0 %88
Test Date:2026-04-17 21:56:09
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : final class Utility {
+       2            2 :   Utility._();
+       3              : 
+       4            6 :   static Utility instance = Utility._();
+       5              : 
+       6            2 :   Future<bool> completedWithinDeadline({
+       7              :     required Duration deadline,
+       8              :     required Future<bool> Function() future,
+       9              :     Duration tick = const Duration(milliseconds: 100),
+      10              :   }) async {
+      11            4 :     final Stopwatch stopwatch = Stopwatch()..start();
+      12              :     bool completed = false;
+      13            4 :     await Future.doWhile(() async {
+      14            2 :       completed = await future();
+      15              :       if (completed ||
+      16            3 :           stopwatch.elapsedMilliseconds > deadline.inMilliseconds) {
+      17              :         return false;
+      18              :       } else {
+      19            1 :         await Future.delayed(tick);
+      20              :         return true;
+      21              :       }
+      22              :     });
+      23              : 
+      24              :     return completed;
+      25              :   }
+      26              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.3.1-1
+
+ + + diff --git a/coverage/html/ruby.png b/coverage/html/ruby.png new file mode 100644 index 00000000..991b6d4e Binary files /dev/null and b/coverage/html/ruby.png differ diff --git a/coverage/html/snow.png b/coverage/html/snow.png new file mode 100644 index 00000000..2cdae107 Binary files /dev/null and b/coverage/html/snow.png differ diff --git a/coverage/html/updown.png b/coverage/html/updown.png new file mode 100644 index 00000000..aa56a238 Binary files /dev/null and b/coverage/html/updown.png differ diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 00000000..5c4ea2b8 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,673 @@ +SF:lib/flutter_foreground_task.dart +DA:45,6 +DA:46,6 +DA:58,1 +DA:59,1 +DA:63,2 +DA:65,4 +DA:67,1 +DA:69,2 +DA:71,2 +DA:73,4 +DA:75,1 +DA:77,2 +DA:79,2 +DA:81,4 +DA:83,1 +DA:85,2 +DA:87,2 +DA:88,4 +DA:90,1 +DA:91,2 +DA:93,1 +DA:95,2 +DA:97,2 +DA:99,4 +DA:102,2 +DA:103,2 +DA:106,5 +DA:108,10 +DA:112,1 +DA:117,2 +DA:125,1 +DA:135,2 +DA:148,1 +DA:149,2 +DA:153,1 +DA:162,2 +DA:174,1 +DA:175,2 +DA:178,1 +DA:180,2 +DA:184,6 +DA:189,0 +DA:190,0 +DA:194,2 +DA:195,4 +DA:197,1 +DA:198,2 +DA:200,2 +DA:202,4 +DA:204,1 +DA:206,2 +DA:208,1 +DA:209,2 +DA:212,2 +DA:213,4 +DA:217,2 +DA:218,4 +DA:222,1 +DA:223,2 +DA:227,3 +DA:234,1 +DA:235,3 +DA:241,1 +DA:242,2 +DA:245,3 +DA:248,1 +DA:252,2 +DA:255,1 +DA:256,2 +DA:259,3 +DA:264,6 +DA:273,3 +DA:276,1 +DA:277,2 +DA:280,3 +DA:283,3 +DA:286,1 +DA:287,2 +DA:290,1 +DA:291,2 +DA:296,1 +DA:297,2 +DA:300,3 +DA:303,1 +DA:304,2 +DA:307,1 +DA:308,2 +DA:311,1 +DA:312,2 +DA:315,1 +DA:316,2 +DA:325,1 +DA:326,2 +DA:341,1 +DA:344,2 +DA:350,1 +DA:351,2 +LF:97 +LH:95 +end_of_record +SF:lib/errors/service_already_started_exception.dart +DA:2,2 +DA:7,1 +DA:8,2 +LF:3 +LH:3 +end_of_record +SF:lib/errors/service_not_initialized_exception.dart +DA:2,2 +DA:8,1 +DA:9,2 +LF:3 +LH:3 +end_of_record +SF:lib/errors/service_not_started_exception.dart +DA:2,2 +DA:6,1 +DA:7,2 +LF:3 +LH:3 +end_of_record +SF:lib/errors/service_timeout_exception.dart +DA:2,2 +DA:8,1 +DA:9,2 +LF:3 +LH:3 +end_of_record +SF:lib/flutter_foreground_task_controller.dart +DA:33,6 +DA:38,18 +DA:41,6 +DA:42,24 +DA:50,1 +DA:55,1 +DA:60,4 +DA:61,4 +DA:76,5 +DA:77,5 +DA:78,5 +DA:79,5 +DA:80,5 +DA:81,5 +DA:83,7 +DA:84,5 +DA:85,7 +DA:86,5 +DA:87,10 +DA:95,2 +DA:100,2 +DA:101,2 +DA:102,2 +DA:103,2 +DA:107,1 +DA:118,1 +DA:119,1 +DA:122,1 +DA:123,1 +DA:126,2 +DA:127,1 +DA:128,1 +DA:129,1 +DA:130,1 +DA:141,1 +DA:142,1 +DA:147,1 +DA:152,1 +DA:154,1 +DA:155,1 +DA:158,3 +DA:162,1 +DA:167,1 +DA:177,1 +DA:178,1 +DA:181,2 +DA:182,1 +DA:194,1 +DA:199,1 +DA:201,1 +DA:202,1 +DA:205,3 +DA:207,1 +DA:208,1 +DA:213,1 +DA:217,2 +DA:218,4 +DA:220,2 +DA:221,4 +DA:226,1 +DA:231,3 +DA:232,9 +DA:238,0 +DA:239,0 +DA:249,6 +DA:252,2 +DA:253,2 +DA:254,2 +DA:256,4 +DA:257,4 +DA:258,2 +DA:259,2 +DA:261,2 +DA:262,7 +DA:263,3 +DA:264,1 +DA:271,2 +DA:272,4 +DA:273,4 +DA:278,1 +DA:279,2 +DA:283,1 +DA:284,3 +DA:287,1 +DA:289,2 +DA:290,1 +DA:299,1 +DA:300,1 +DA:301,1 +DA:303,2 +DA:305,1 +DA:309,1 +DA:310,1 +DA:311,1 +DA:313,1 +DA:314,2 +DA:315,1 +DA:316,1 +DA:318,1 +DA:319,1 +DA:328,1 +DA:332,1 +DA:333,1 +DA:335,1 +DA:336,1 +DA:337,1 +DA:339,1 +DA:340,1 +DA:342,1 +DA:343,1 +DA:345,1 +DA:346,1 +DA:352,1 +DA:353,1 +DA:354,1 +DA:356,2 +DA:360,1 +DA:361,1 +DA:362,1 +DA:364,2 +DA:365,1 +DA:366,1 +LF:122 +LH:120 +end_of_record +SF:lib/flutter_foreground_task_platform_interface.dart +DA:14,18 +DA:16,18 +DA:18,7 +DA:19,1 +DA:24,12 +DA:29,6 +DA:30,12 +DA:36,1 +DA:50,1 +DA:53,1 +DA:54,1 +DA:57,1 +DA:67,1 +DA:70,1 +DA:71,1 +DA:74,1 +DA:75,1 +DA:78,1 +DA:79,1 +DA:82,1 +DA:83,1 +DA:88,1 +DA:89,1 +DA:94,1 +DA:95,1 +DA:98,1 +DA:99,1 +DA:102,1 +DA:103,1 +DA:107,1 +DA:108,1 +DA:111,1 +DA:112,1 +DA:115,1 +DA:116,1 +DA:120,1 +DA:121,1 +DA:125,1 +DA:126,1 +DA:130,1 +DA:131,1 +DA:134,1 +DA:135,1 +DA:139,1 +DA:140,1 +DA:144,1 +DA:145,1 +DA:149,1 +DA:150,1 +DA:154,1 +DA:155,1 +DA:161,1 +DA:164,1 +DA:168,1 +DA:169,1 +LF:55 +LH:55 +end_of_record +SF:lib/models/foreground_service_types.dart +DA:4,3 +LF:1 +LH:1 +end_of_record +SF:lib/models/foreground_task_options.dart +DA:6,4 +DA:47,2 +DA:48,2 +DA:49,6 +DA:50,4 +DA:51,4 +DA:52,4 +DA:53,4 +DA:54,4 +DA:55,4 +DA:62,1 +DA:71,1 +DA:72,1 +DA:73,1 +DA:74,1 +DA:75,1 +DA:76,1 +DA:77,1 +DA:78,1 +LF:19 +LH:19 +end_of_record +SF:lib/models/notification_button.dart +DA:8,1 +DA:12,3 +DA:13,3 +DA:25,2 +DA:26,2 +DA:27,2 +DA:28,2 +DA:29,4 +DA:34,1 +DA:39,1 +DA:40,1 +DA:41,1 +DA:42,1 +LF:13 +LH:13 +end_of_record +SF:lib/models/notification_icon.dart +DA:8,1 +DA:11,3 +DA:20,2 +DA:21,2 +DA:22,2 +DA:23,4 +DA:28,1 +DA:32,1 +DA:33,1 +DA:34,1 +LF:10 +LH:10 +end_of_record +SF:lib/models/notification_options.dart +DA:8,4 +DA:22,8 +DA:23,8 +DA:94,2 +DA:95,2 +DA:96,4 +DA:97,4 +DA:98,4 +DA:99,4 +DA:100,6 +DA:101,6 +DA:102,4 +DA:103,4 +DA:104,4 +DA:105,4 +DA:106,4 +DA:107,6 +DA:108,4 +DA:113,1 +DA:127,1 +DA:128,1 +DA:129,1 +DA:130,1 +DA:131,1 +DA:132,1 +DA:133,1 +DA:134,1 +DA:135,1 +DA:136,1 +DA:137,1 +DA:138,1 +DA:139,1 +DA:146,2 +DA:186,3 +DA:187,3 +DA:188,6 +DA:189,6 +DA:190,3 +DA:191,3 +DA:192,5 +DA:197,1 +DA:203,1 +DA:204,1 +DA:205,1 +DA:207,1 +DA:208,1 +DA:258,2 +DA:264,6 +DA:265,6 +DA:285,1 +DA:286,1 +DA:287,1 +DA:288,1 +DA:289,1 +DA:290,2 +DA:295,1 +DA:301,1 +DA:302,1 +DA:303,0 +DA:304,1 +DA:305,1 +LF:61 +LH:60 +end_of_record +SF:lib/models/notification_permission.dart +DA:12,2 +DA:13,2 +LF:2 +LH:2 +end_of_record +SF:lib/models/service_request_result.dart +DA:3,4 +DA:8,3 +DA:13,2 +LF:3 +LH:3 +end_of_record +SF:lib/task_handler.dart +DA:19,1 +DA:22,1 +DA:25,1 +DA:32,1 +DA:43,4 +LF:5 +LH:5 +end_of_record +SF:lib/models/foreground_task_event_action.dart +DA:3,4 +DA:9,3 +DA:10,3 +DA:13,1 +DA:14,1 +DA:17,2 +DA:18,2 +DA:28,2 +DA:29,2 +DA:30,4 +DA:31,2 +LF:11 +LH:11 +end_of_record +SF:lib/models/notification_channel_importance.dart +DA:5,3 +LF:1 +LH:1 +end_of_record +SF:lib/models/notification_priority.dart +DA:4,3 +LF:1 +LH:1 +end_of_record +SF:lib/models/notification_visibility.dart +DA:4,3 +LF:1 +LH:1 +end_of_record +SF:lib/ui/with_foreground_task.dart +DA:13,1 +DA:15,1 +DA:16,1 +DA:20,1 +DA:23,1 +DA:24,1 +DA:26,1 +DA:30,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,2 +LF:12 +LH:12 +end_of_record +SF:lib/utils/utility.dart +DA:2,2 +DA:4,6 +DA:6,2 +DA:11,4 +DA:13,4 +DA:14,2 +DA:16,3 +DA:19,1 +LF:8 +LH:8 +end_of_record +SF:lib/flutter_foreground_task_method_channel.dart +DA:36,1 +DA:51,1 +DA:64,2 +DA:66,2 +DA:69,1 +DA:71,3 +DA:74,1 +DA:85,1 +DA:94,2 +DA:96,2 +DA:99,1 +DA:101,3 +DA:104,3 +DA:106,3 +DA:107,6 +DA:110,1 +DA:112,2 +DA:113,2 +DA:118,0 +DA:121,0 +DA:122,0 +DA:125,0 +DA:126,0 +DA:129,0 +DA:132,2 +DA:134,2 +DA:136,2 +DA:137,2 +DA:138,1 +DA:139,1 +DA:140,1 +DA:143,2 +DA:144,2 +DA:145,1 +DA:147,2 +DA:148,1 +DA:150,2 +DA:151,2 +DA:152,2 +DA:154,1 +DA:155,1 +DA:156,3 +DA:158,2 +DA:160,0 +DA:163,1 +DA:165,1 +DA:166,2 +DA:167,1 +DA:169,1 +DA:170,1 +DA:172,1 +DA:173,1 +DA:180,2 +DA:182,6 +DA:190,2 +DA:192,4 +DA:195,1 +DA:197,2 +DA:198,2 +DA:202,1 +DA:204,2 +DA:205,2 +DA:209,1 +DA:211,2 +DA:214,1 +DA:216,2 +DA:217,2 +DA:221,1 +DA:223,2 +DA:224,2 +DA:229,1 +DA:231,2 +DA:232,1 +DA:233,1 +DA:238,1 +DA:240,2 +DA:241,2 +DA:246,1 +DA:248,2 +DA:249,2 +DA:254,1 +DA:256,2 +DA:257,2 +DA:262,1 +DA:265,2 +DA:266,1 +DA:269,1 +DA:272,2 +DA:273,1 +DA:276,1 +DA:278,2 +DA:279,2 +DA:284,1 +DA:286,2 +DA:287,2 +DA:294,1 +DA:298,2 +DA:301,1 +DA:303,2 +DA:304,2 +DA:306,1 +DA:310,1 +DA:312,2 +DA:315,1 +DA:316,1 +LF:105 +LH:98 +end_of_record +SF:lib/models/service_options.dart +DA:12,1 +DA:40,1 +DA:41,1 +DA:42,2 +DA:43,2 +DA:44,2 +DA:45,2 +DA:46,2 +DA:47,3 +DA:48,6 +DA:49,2 +DA:52,1 +DA:53,2 +DA:56,1 +DA:57,3 +DA:58,1 +DA:59,3 +DA:62,1 +DA:63,1 +DA:64,3 +DA:72,1 +DA:92,1 +DA:93,1 +DA:94,1 +DA:95,1 +DA:96,1 +DA:97,2 +DA:98,5 +DA:99,1 +DA:102,1 +DA:103,3 +DA:106,1 +DA:107,1 +DA:108,3 +LF:34 +LH:34 +end_of_record +SF:lib/utils/color_extension.dart +DA:4,4 +DA:5,8 +DA:6,8 +DA:7,8 +LF:4 +LH:4 +end_of_record diff --git a/lib/flutter_foreground_task_controller.dart b/lib/flutter_foreground_task_controller.dart index a6665d0d..3351d3e3 100644 --- a/lib/flutter_foreground_task_controller.dart +++ b/lib/flutter_foreground_task_controller.dart @@ -335,15 +335,17 @@ class FlutterForegroundTaskController { final String prefsKey = _kPrefsKeyPrefix + key; if (value is int) { return prefs.setInt(prefsKey, value); - } else if (value is double) { + } + if (value is double) { return prefs.setDouble(prefsKey, value); - } else if (value is String) { + } + if (value is String) { return prefs.setString(prefsKey, value); - } else if (value is bool) { + } + if (value is bool) { return prefs.setBool(prefsKey, value); - } else { - return false; } + return false; } /// Remove data with [key]. diff --git a/lib/ui/with_foreground_task.dart b/lib/ui/with_foreground_task.dart index 87a3ff84..5dc1e832 100644 --- a/lib/ui/with_foreground_task.dart +++ b/lib/ui/with_foreground_task.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; -/// A widget that minimize the app without closing it when the user presses the soft back button. -/// It only works when the service is running. +/// A widget that minimizes the app without closing it when the user presses +/// the back button while the foreground service is running. /// /// This widget must be declared above the [Scaffold] widget. class WithForegroundTask extends StatefulWidget { @@ -16,17 +17,22 @@ class WithForegroundTask extends StatefulWidget { } class _WithForegroundTaskState extends State { - Future _onWillPop() async { - final bool canPop = mounted ? Navigator.canPop(context) : false; - if (!canPop && await FlutterForegroundTask.isRunningService) { + Future _onPopInvokedWithResult(bool didPop, dynamic result) async { + if (didPop) return; + + if (await FlutterForegroundTask.isRunningService) { FlutterForegroundTask.minimizeApp(); - return false; + } else { + SystemNavigator.pop(); } - - return true; } @override - Widget build(BuildContext context) => - WillPopScope(onWillPop: _onWillPop, child: widget.child); + Widget build(BuildContext context) { + return PopScope( + canPop: Navigator.canPop(context), + onPopInvokedWithResult: _onPopInvokedWithResult, + child: widget.child, + ); + } } diff --git a/lib/utils/color_extension.dart b/lib/utils/color_extension.dart index 412a07cd..51c25529 100644 --- a/lib/utils/color_extension.dart +++ b/lib/utils/color_extension.dart @@ -1,5 +1,8 @@ import 'dart:ui'; extension ColorExtension on Color { - String get toRgbString => '$red,$green,$blue'; + String get toRgbString => + '${(r * 255.0).round().clamp(0, 255)},' + '${(g * 255.0).round().clamp(0, 255)},' + '${(b * 255.0).round().clamp(0, 255)}'; } diff --git a/test/exceptions_test.dart b/test/exceptions_test.dart new file mode 100644 index 00000000..62c28ca5 --- /dev/null +++ b/test/exceptions_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ServiceAlreadyStartedException', () { + test('default message', () { + final e = ServiceAlreadyStartedException(); + expect(e.message, 'The service has already started.'); + }); + + test('custom message', () { + final e = ServiceAlreadyStartedException('custom'); + expect(e.message, 'custom'); + }); + + test('toString', () { + final e = ServiceAlreadyStartedException(); + expect( + e.toString(), + 'ServiceAlreadyStartedException: The service has already started.', + ); + }); + + test('implements Exception', () { + expect(ServiceAlreadyStartedException(), isA()); + }); + }); + + group('ServiceNotInitializedException', () { + test('default message', () { + final e = ServiceNotInitializedException(); + expect( + e.message, + 'Not initialized. Please call this function after calling the init function.', + ); + }); + + test('custom message', () { + final e = ServiceNotInitializedException('custom'); + expect(e.message, 'custom'); + }); + + test('toString', () { + final e = ServiceNotInitializedException(); + expect( + e.toString(), + contains('ServiceNotInitializedException:'), + ); + }); + + test('implements Exception', () { + expect(ServiceNotInitializedException(), isA()); + }); + }); + + group('ServiceNotStartedException', () { + test('default message', () { + final e = ServiceNotStartedException(); + expect(e.message, 'The service is not started.'); + }); + + test('custom message', () { + final e = ServiceNotStartedException('stopped'); + expect(e.message, 'stopped'); + }); + + test('toString', () { + final e = ServiceNotStartedException(); + expect( + e.toString(), + 'ServiceNotStartedException: The service is not started.', + ); + }); + + test('implements Exception', () { + expect(ServiceNotStartedException(), isA()); + }); + }); + + group('ServiceTimeoutException', () { + test('default message', () { + final e = ServiceTimeoutException(); + expect(e.message, contains('timed out')); + }); + + test('custom message', () { + final e = ServiceTimeoutException('timeout'); + expect(e.message, 'timeout'); + }); + + test('toString', () { + final e = ServiceTimeoutException(); + expect(e.toString(), contains('ServiceTimeoutException:')); + }); + + test('implements Exception', () { + expect(ServiceTimeoutException(), isA()); + }); + }); +} diff --git a/test/method_channel_test.dart b/test/method_channel_test.dart new file mode 100644 index 00000000..421a1431 --- /dev/null +++ b/test/method_channel_test.dart @@ -0,0 +1,305 @@ +import 'dart:isolate'; + +import 'package:flutter/services.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_method_channel.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform/platform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MethodChannelFlutterForegroundTask platformChannel; + late _MethodCallHandler methodCallHandler; + + setUp(() { + platformChannel = MethodChannelFlutterForegroundTask(); + FlutterForegroundTaskPlatform.instance = platformChannel; + FlutterForegroundTask.resetStatic(); + + methodCallHandler = _MethodCallHandler(); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mMDChannel, + methodCallHandler.onMethodCall, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformChannel.mMDChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformChannel.mBGChannel, null); + }); + + group('onBackgroundChannel', () { + late _TestTaskHandler taskHandler; + + setUp(() { + taskHandler = _TestTaskHandler(); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mBGChannel, + (MethodCall methodCall) { + return platformChannel.onBackgroundChannel( + methodCall, taskHandler); + }, + ); + }); + + test('onServiceIdSet sets currentServiceId', () async { + await platformChannel.mBGChannel + .invokeMethod('onServiceIdSet', 'my_service'); + expect(FlutterForegroundTaskController.currentServiceId, 'my_service'); + }); + + test('onServiceIdSet ignores non-string argument', () async { + FlutterForegroundTaskController.setCurrentServiceId('original'); + await platformChannel.mBGChannel.invokeMethod('onServiceIdSet', 42); + expect( + FlutterForegroundTaskController.currentServiceId, + 'original', + ); + }); + + test('onDestroy defaults isTimeout to false when null', () async { + await platformChannel.mBGChannel.invokeMethod('onDestroy', null); + expect(taskHandler.destroyTimeoutValues.last, false); + }); + + test('onDestroy passes isTimeout true', () async { + await platformChannel.mBGChannel.invokeMethod('onDestroy', true); + expect(taskHandler.destroyTimeoutValues.last, true); + }); + }); + + group('attachedActivity', () { + test('returns true on Android when channel returns true', () async { + platformChannel.platform = + FakePlatform(operatingSystem: Platform.android); + + final result = await platformChannel.attachedActivity; + expect(result, true); + }); + + test('returns true on iOS without calling channel', () async { + platformChannel.platform = + FakePlatform(operatingSystem: Platform.iOS); + + final result = await platformChannel.attachedActivity; + expect(result, true); + expect( + methodCallHandler.log + .where((c) => c.method == 'attachedActivity') + .isEmpty, + true, + ); + }); + }); + + group('sendDataToTask', () { + test('invokes sendData with serviceId', () { + platformChannel.platform = + FakePlatform(operatingSystem: Platform.android); + + platformChannel.sendDataToTask('hello', serviceId: 'svc1'); + expect(methodCallHandler.log.last.method, 'sendData'); + final args = + methodCallHandler.log.last.arguments as Map; + expect(args['serviceId'], 'svc1'); + expect(args['data'], 'hello'); + }); + }); + + group('FlutterForegroundTaskController', () { + test('of returns same instance for same id', () { + final a = FlutterForegroundTaskController.of('test_id'); + final b = FlutterForegroundTaskController.of('test_id'); + expect(identical(a, b), true); + }); + + test('of returns different instance for different id', () { + final a = FlutterForegroundTaskController.of('id_a'); + final b = FlutterForegroundTaskController.of('id_b'); + expect(identical(a, b), false); + }); + + test('setCurrentServiceId and getter', () { + FlutterForegroundTaskController.setCurrentServiceId('custom_svc'); + expect( + FlutterForegroundTaskController.currentServiceId, + 'custom_svc', + ); + }); + + test('resetState clears all fields', () { + final ctrl = FlutterForegroundTaskController.of('reset_test'); + ctrl.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'c', + channelName: 'n', + ), + iosNotificationOptions: const IOSNotificationOptions(), + foregroundTaskOptions: ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ), + ); + expect(ctrl.isInitialized, true); + + ctrl.resetState(); + expect(ctrl.isInitialized, false); + expect(ctrl.androidNotificationOptions, isNull); + expect(ctrl.iosNotificationOptions, isNull); + expect(ctrl.foregroundTaskOptions, isNull); + expect(ctrl.skipServiceResponseCheck, false); + }); + + test('sendDataToMain delivers via IsolateNameServer', () async { + final ctrl = FlutterForegroundTaskController.of('send_main_test'); + ctrl.initCommunicationPort(); + + final received = []; + ctrl.addTaskDataCallback((data) => received.add(data)); + + ctrl.sendDataToMain('hello from task'); + + // Give the message time to arrive through the port. + await Future.delayed(const Duration(milliseconds: 50)); + expect(received, contains('hello from task')); + + ctrl.resetState(); + }); + + test('sendDataToMain is a no-op when no port is registered', () { + final ctrl = FlutterForegroundTaskController.of('no_port_test'); + // No initCommunicationPort() called, so no SendPort is registered. + // Should not throw. + ctrl.sendDataToMain('ignored'); + ctrl.resetState(); + }); + }); + + group('FlutterForegroundTask static API coverage', () { + test('currentServiceId returns controller value', () { + FlutterForegroundTaskController.setCurrentServiceId('static_test'); + expect(FlutterForegroundTask.currentServiceId, 'static_test'); + }); + + test('androidNotificationOptions setter and getter', () { + expect(FlutterForegroundTask.androidNotificationOptions, isNull); + + final opts = AndroidNotificationOptions( + channelId: 'ch', + channelName: 'name', + ); + FlutterForegroundTask.androidNotificationOptions = opts; + expect(FlutterForegroundTask.androidNotificationOptions, same(opts)); + }); + + test('iosNotificationOptions setter and getter', () { + expect(FlutterForegroundTask.iosNotificationOptions, isNull); + + const opts = IOSNotificationOptions(showNotification: false); + FlutterForegroundTask.iosNotificationOptions = opts; + expect(FlutterForegroundTask.iosNotificationOptions, same(opts)); + }); + + test('foregroundTaskOptions setter and getter', () { + expect(FlutterForegroundTask.foregroundTaskOptions, isNull); + + final opts = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ); + FlutterForegroundTask.foregroundTaskOptions = opts; + expect(FlutterForegroundTask.foregroundTaskOptions, same(opts)); + }); + + test('isInitialized setter', () { + expect(FlutterForegroundTask.isInitialized, false); + FlutterForegroundTask.isInitialized = true; + expect(FlutterForegroundTask.isInitialized, true); + }); + + test('skipServiceResponseCheck getter', () { + expect(FlutterForegroundTask.skipServiceResponseCheck, false); + FlutterForegroundTask.skipServiceResponseCheck = true; + expect(FlutterForegroundTask.skipServiceResponseCheck, true); + }); + + test('receivePort setter', () { + final port = ReceivePort(); + FlutterForegroundTask.receivePort = port; + expect(FlutterForegroundTask.receivePort, same(port)); + port.close(); + }); + + test('streamSubscription getter and setter', () { + expect(FlutterForegroundTask.streamSubscription, isNull); + FlutterForegroundTask.streamSubscription = null; + expect(FlutterForegroundTask.streamSubscription, isNull); + }); + + test('checkServiceStateChange succeeds when already at target', () async { + platformChannel.platform = + FakePlatform(operatingSystem: Platform.android); + + // isRunningService returns false (from mock), target is false -> succeeds + await FlutterForegroundTask.checkServiceStateChange(target: false); + }); + + test('sendDataToMain delivers data via controller', () async { + FlutterForegroundTask.initCommunicationPort(); + + final received = []; + FlutterForegroundTask.addTaskDataCallback( + (data) => received.add(data)); + + FlutterForegroundTask.sendDataToMain('static_msg'); + + await Future.delayed(const Duration(milliseconds: 50)); + expect(received, contains('static_msg')); + }); + }); +} + +class _MethodCallHandler { + _MethodCallHandler(); + + final List log = []; + + Future? onMethodCall(MethodCall methodCall) async { + log.add(methodCall); + + switch (methodCall.method) { + case 'attachedActivity': + return true; + case 'isRunningService': + return false; + case 'sendData': + return null; + default: + return null; + } + } +} + +class _TestTaskHandler extends TaskHandler { + final List startStarterIndices = []; + final List destroyTimeoutValues = []; + + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + startStarterIndices.add(starter.index); + } + + @override + void onRepeatEvent(DateTime timestamp) {} + + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + destroyTimeoutValues.add(isTimeout); + } +} diff --git a/test/models_test.dart b/test/models_test.dart new file mode 100644 index 00000000..b5141002 --- /dev/null +++ b/test/models_test.dart @@ -0,0 +1,540 @@ +import 'dart:ui'; + +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ForegroundTaskEventAction', () { + test('nothing() factory', () { + final action = ForegroundTaskEventAction.nothing(); + expect(action.type, ForegroundTaskEventType.nothing); + expect(action.interval, isNull); + }); + + test('once() factory', () { + final action = ForegroundTaskEventAction.once(); + expect(action.type, ForegroundTaskEventType.once); + expect(action.interval, isNull); + }); + + test('repeat() factory', () { + final action = ForegroundTaskEventAction.repeat(5000); + expect(action.type, ForegroundTaskEventType.repeat); + expect(action.interval, 5000); + }); + + test('toJson for nothing', () { + final json = ForegroundTaskEventAction.nothing().toJson(); + expect(json['taskEventType'], ForegroundTaskEventType.nothing.value); + expect(json['taskEventInterval'], isNull); + }); + + test('toJson for once', () { + final json = ForegroundTaskEventAction.once().toJson(); + expect(json['taskEventType'], ForegroundTaskEventType.once.value); + expect(json['taskEventInterval'], isNull); + }); + + test('toJson for repeat', () { + final json = ForegroundTaskEventAction.repeat(2000).toJson(); + expect(json['taskEventType'], ForegroundTaskEventType.repeat.value); + expect(json['taskEventInterval'], 2000); + }); + }); + + group('ForegroundTaskEventType', () { + test('values', () { + expect(ForegroundTaskEventType.nothing.value, 1); + expect(ForegroundTaskEventType.once.value, 2); + expect(ForegroundTaskEventType.repeat.value, 3); + }); + }); + + group('NotificationButton', () { + test('constructor sets fields', () { + const button = NotificationButton(id: 'btn1', text: 'Click'); + expect(button.id, 'btn1'); + expect(button.text, 'Click'); + expect(button.textColor, isNull); + }); + + test('constructor with textColor', () { + const button = NotificationButton( + id: 'btn1', + text: 'Click', + textColor: Color(0xFFFF0000), + ); + expect(button.textColor, const Color(0xFFFF0000)); + }); + + test('toJson without textColor', () { + const button = NotificationButton(id: 'btn1', text: 'Click'); + final json = button.toJson(); + expect(json['id'], 'btn1'); + expect(json['text'], 'Click'); + expect(json['textColorRgb'], isNull); + }); + + test('toJson with textColor', () { + const button = NotificationButton( + id: 'btn1', + text: 'Click', + textColor: Color(0xFFFF0000), + ); + final json = button.toJson(); + expect(json['id'], 'btn1'); + expect(json['text'], 'Click'); + expect(json['textColorRgb'], isNotNull); + }); + + test('copyWith replaces fields', () { + const original = NotificationButton( + id: 'btn1', + text: 'Click', + textColor: Color(0xFFFF0000), + ); + final copy = original.copyWith(id: 'btn2'); + expect(copy.id, 'btn2'); + expect(copy.text, 'Click'); + expect(copy.textColor, const Color(0xFFFF0000)); + }); + + test('copyWith preserves all fields when no args', () { + const original = NotificationButton( + id: 'btn1', + text: 'Click', + textColor: Color(0xFF00FF00), + ); + final copy = original.copyWith(); + expect(copy.id, original.id); + expect(copy.text, original.text); + expect(copy.textColor, original.textColor); + }); + + test('copyWith replaces text', () { + const original = NotificationButton(id: 'btn1', text: 'Click'); + final copy = original.copyWith(text: 'Press'); + expect(copy.text, 'Press'); + }); + + test('copyWith replaces textColor', () { + const original = NotificationButton(id: 'btn1', text: 'Click'); + final copy = original.copyWith(textColor: const Color(0xFF0000FF)); + expect(copy.textColor, const Color(0xFF0000FF)); + }); + }); + + group('NotificationIcon', () { + test('constructor sets fields', () { + const icon = NotificationIcon(metaDataName: 'icon_meta'); + expect(icon.metaDataName, 'icon_meta'); + expect(icon.backgroundColor, isNull); + }); + + test('constructor with backgroundColor', () { + const icon = NotificationIcon( + metaDataName: 'icon_meta', + backgroundColor: Color(0xFF00FF00), + ); + expect(icon.backgroundColor, const Color(0xFF00FF00)); + }); + + test('toJson without backgroundColor', () { + const icon = NotificationIcon(metaDataName: 'icon_meta'); + final json = icon.toJson(); + expect(json['metaDataName'], 'icon_meta'); + expect(json['backgroundColorRgb'], isNull); + }); + + test('toJson with backgroundColor', () { + const icon = NotificationIcon( + metaDataName: 'icon_meta', + backgroundColor: Color(0xFF00FF00), + ); + final json = icon.toJson(); + expect(json['metaDataName'], 'icon_meta'); + expect(json['backgroundColorRgb'], isNotNull); + }); + + test('copyWith replaces metaDataName', () { + const original = NotificationIcon(metaDataName: 'icon1'); + final copy = original.copyWith(metaDataName: 'icon2'); + expect(copy.metaDataName, 'icon2'); + expect(copy.backgroundColor, isNull); + }); + + test('copyWith replaces backgroundColor', () { + const original = NotificationIcon(metaDataName: 'icon1'); + final copy = + original.copyWith(backgroundColor: const Color(0xFFFF0000)); + expect(copy.metaDataName, 'icon1'); + expect(copy.backgroundColor, const Color(0xFFFF0000)); + }); + + test('copyWith preserves all fields when no args', () { + const original = NotificationIcon( + metaDataName: 'icon1', + backgroundColor: Color(0xFF0000FF), + ); + final copy = original.copyWith(); + expect(copy.metaDataName, original.metaDataName); + expect(copy.backgroundColor, original.backgroundColor); + }); + }); + + group('ForegroundTaskOptions', () { + test('constructor defaults', () { + final options = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ); + expect(options.autoRunOnBoot, false); + expect(options.autoRunOnMyPackageReplaced, false); + expect(options.allowWakeLock, true); + expect(options.allowWifiLock, false); + expect(options.allowAutoRestart, true); + expect(options.stopWithTask, isNull); + }); + + test('toJson includes all fields', () { + final options = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(1000), + autoRunOnBoot: true, + autoRunOnMyPackageReplaced: true, + allowWakeLock: false, + allowWifiLock: true, + allowAutoRestart: false, + stopWithTask: true, + ); + final json = options.toJson(); + expect(json['autoRunOnBoot'], true); + expect(json['autoRunOnMyPackageReplaced'], true); + expect(json['allowWakeLock'], false); + expect(json['allowWifiLock'], true); + expect(json['allowAutoRestart'], false); + expect(json['stopWithTask'], true); + expect(json['taskEventAction'], isA()); + }); + + test('toJson omits stopWithTask when null', () { + final options = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ); + final json = options.toJson(); + expect(json.containsKey('stopWithTask'), isFalse); + }); + + test('copyWith replaces eventAction', () { + final original = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ); + final copy = original.copyWith( + eventAction: ForegroundTaskEventAction.once(), + ); + expect(copy.eventAction.type, ForegroundTaskEventType.once); + expect(copy.autoRunOnBoot, original.autoRunOnBoot); + }); + + test('copyWith replaces boolean fields', () { + final original = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ); + final copy = original.copyWith( + autoRunOnBoot: true, + autoRunOnMyPackageReplaced: true, + allowWakeLock: false, + allowWifiLock: true, + allowAutoRestart: false, + ); + expect(copy.autoRunOnBoot, true); + expect(copy.autoRunOnMyPackageReplaced, true); + expect(copy.allowWakeLock, false); + expect(copy.allowWifiLock, true); + expect(copy.allowAutoRestart, false); + }); + + test('copyWith replaces stopWithTask', () { + final original = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ); + final copy = original.copyWith(stopWithTask: true); + expect(copy.stopWithTask, true); + }); + + test('copyWith can set stopWithTask to null', () { + final original = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + stopWithTask: true, + ); + final copy = original.copyWith(stopWithTask: null); + expect(copy.stopWithTask, isNull); + }); + + test('copyWith preserves all fields when no args', () { + final original = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(500), + autoRunOnBoot: true, + stopWithTask: false, + ); + final copy = original.copyWith(); + expect(copy.eventAction.type, original.eventAction.type); + expect(copy.eventAction.interval, original.eventAction.interval); + expect(copy.autoRunOnBoot, original.autoRunOnBoot); + expect(copy.stopWithTask, original.stopWithTask); + }); + }); + + group('AndroidNotificationOptions', () { + test('constructor defaults', () { + final options = AndroidNotificationOptions( + channelId: 'ch1', + channelName: 'Channel 1', + ); + expect(options.channelImportance, NotificationChannelImportance.LOW); + expect(options.priority, NotificationPriority.LOW); + expect(options.enableVibration, false); + expect(options.playSound, false); + expect(options.showWhen, false); + expect(options.showBadge, false); + expect(options.onlyAlertOnce, false); + expect(options.visibility, NotificationVisibility.VISIBILITY_PUBLIC); + expect(options.storagePrefix, isNull); + }); + + test('toJson includes all fields', () { + final options = AndroidNotificationOptions( + channelId: 'ch1', + channelName: 'Channel 1', + channelDescription: 'desc', + channelImportance: NotificationChannelImportance.HIGH, + priority: NotificationPriority.HIGH, + enableVibration: true, + playSound: true, + showWhen: true, + showBadge: true, + onlyAlertOnce: true, + visibility: NotificationVisibility.VISIBILITY_PRIVATE, + storagePrefix: 'prefix_', + ); + final json = options.toJson(); + expect(json['notificationChannelId'], 'ch1'); + expect(json['notificationChannelName'], 'Channel 1'); + expect(json['notificationChannelDescription'], 'desc'); + expect(json['enableVibration'], true); + expect(json['playSound'], true); + expect(json['showWhen'], true); + expect(json['showBadge'], true); + expect(json['onlyAlertOnce'], true); + expect(json['storagePrefix'], 'prefix_'); + }); + + test('toJson omits storagePrefix when null', () { + final options = AndroidNotificationOptions( + channelId: 'ch1', + channelName: 'Channel 1', + ); + final json = options.toJson(); + expect(json.containsKey('storagePrefix'), isFalse); + }); + + test('copyWith replaces fields', () { + final original = AndroidNotificationOptions( + channelId: 'ch1', + channelName: 'Channel 1', + ); + final copy = original.copyWith( + channelId: 'ch2', + channelName: 'Channel 2', + channelDescription: 'new desc', + channelImportance: NotificationChannelImportance.MAX, + priority: NotificationPriority.MAX, + enableVibration: true, + playSound: true, + showWhen: true, + showBadge: true, + onlyAlertOnce: true, + visibility: NotificationVisibility.VISIBILITY_SECRET, + storagePrefix: 'new_prefix', + ); + expect(copy.channelId, 'ch2'); + expect(copy.channelName, 'Channel 2'); + expect(copy.channelDescription, 'new desc'); + expect(copy.channelImportance, NotificationChannelImportance.MAX); + expect(copy.priority, NotificationPriority.MAX); + expect(copy.enableVibration, true); + expect(copy.playSound, true); + expect(copy.showWhen, true); + expect(copy.showBadge, true); + expect(copy.onlyAlertOnce, true); + expect(copy.visibility, NotificationVisibility.VISIBILITY_SECRET); + expect(copy.storagePrefix, 'new_prefix'); + }); + + test('copyWith preserves all fields when no args', () { + final original = AndroidNotificationOptions( + channelId: 'ch1', + channelName: 'Channel 1', + channelDescription: 'desc', + storagePrefix: 'pfx', + ); + final copy = original.copyWith(); + expect(copy.channelId, original.channelId); + expect(copy.channelName, original.channelName); + expect(copy.channelDescription, original.channelDescription); + expect(copy.storagePrefix, original.storagePrefix); + }); + }); + + group('IOSNotificationOptions', () { + test('constructor defaults', () { + const options = IOSNotificationOptions(); + expect(options.showNotification, true); + expect(options.playSound, false); + expect(options.continuedProcessingTask, isNull); + expect(options.storageSuiteName, isNull); + }); + + test('toJson includes showNotification and playSound', () { + const options = IOSNotificationOptions( + showNotification: false, + playSound: true, + ); + final json = options.toJson(); + expect(json['showNotification'], false); + expect(json['playSound'], true); + }); + + test('toJson omits storageSuiteName when null', () { + const options = IOSNotificationOptions(); + final json = options.toJson(); + expect(json.containsKey('storageSuiteName'), isFalse); + }); + + test('toJson includes storageSuiteName when set', () { + const options = IOSNotificationOptions(storageSuiteName: 'group.app'); + final json = options.toJson(); + expect(json['storageSuiteName'], 'group.app'); + }); + + test('copyWith replaces fields', () { + const original = IOSNotificationOptions(); + final copy = original.copyWith( + showNotification: false, + playSound: true, + storageSuiteName: 'group.test', + ); + expect(copy.showNotification, false); + expect(copy.playSound, true); + expect(copy.storageSuiteName, 'group.test'); + }); + + test('copyWith replaces continuedProcessingTask', () { + const original = IOSNotificationOptions(); + final copy = original.copyWith( + continuedProcessingTask: const IOSContinuedProcessingTaskOptions( + identifier: 'com.example.task', + title: 'Task', + ), + ); + expect(copy.continuedProcessingTask, isNotNull); + expect(copy.continuedProcessingTask!.identifier, 'com.example.task'); + }); + + test('copyWith preserves all fields when no args', () { + const original = IOSNotificationOptions( + showNotification: false, + playSound: true, + storageSuiteName: 'suite', + ); + final copy = original.copyWith(); + expect(copy.showNotification, original.showNotification); + expect(copy.playSound, original.playSound); + expect(copy.storageSuiteName, original.storageSuiteName); + }); + }); + + group('IOSContinuedProcessingSubmissionStrategy', () { + test('rawValues', () { + expect(IOSContinuedProcessingSubmissionStrategy.queue.rawValue, 'queue'); + expect(IOSContinuedProcessingSubmissionStrategy.fail.rawValue, 'fail'); + }); + }); + + group('ServiceRequestResult', () { + test('ServiceRequestSuccess is a ServiceRequestResult', () { + const result = ServiceRequestSuccess(); + expect(result, isA()); + }); + + test('ServiceRequestFailure holds error', () { + final result = ServiceRequestFailure(error: 'some error'); + expect(result, isA()); + expect(result.error, 'some error'); + }); + }); + + group('TaskStarter', () { + test('fromIndex returns developer', () { + expect(TaskStarter.fromIndex(0), TaskStarter.developer); + }); + + test('fromIndex returns system', () { + expect(TaskStarter.fromIndex(1), TaskStarter.system); + }); + }); + + group('NotificationPermission', () { + test('fromIndex covers all values', () { + expect(NotificationPermission.fromIndex(0), NotificationPermission.granted); + expect(NotificationPermission.fromIndex(1), NotificationPermission.denied); + expect(NotificationPermission.fromIndex(2), NotificationPermission.permanently_denied); + }); + }); + + group('NotificationChannelImportance', () { + test('rawValues', () { + expect(NotificationChannelImportance.NONE.rawValue, 0); + expect(NotificationChannelImportance.MIN.rawValue, 1); + expect(NotificationChannelImportance.LOW.rawValue, 2); + expect(NotificationChannelImportance.DEFAULT.rawValue, 3); + expect(NotificationChannelImportance.HIGH.rawValue, 4); + expect(NotificationChannelImportance.MAX.rawValue, 5); + }); + }); + + group('NotificationPriority', () { + test('rawValues', () { + expect(NotificationPriority.MIN.rawValue, -2); + expect(NotificationPriority.LOW.rawValue, -1); + expect(NotificationPriority.DEFAULT.rawValue, 0); + expect(NotificationPriority.HIGH.rawValue, 1); + expect(NotificationPriority.MAX.rawValue, 2); + }); + }); + + group('NotificationVisibility', () { + test('rawValues', () { + expect(NotificationVisibility.VISIBILITY_PUBLIC.rawValue, 1); + expect(NotificationVisibility.VISIBILITY_SECRET.rawValue, -1); + expect(NotificationVisibility.VISIBILITY_PRIVATE.rawValue, 0); + }); + }); + + group('ForegroundServiceTypes', () { + test('rawValues cover all types', () { + expect(ForegroundServiceTypes.camera.rawValue, 0); + expect(ForegroundServiceTypes.connectedDevice.rawValue, 1); + expect(ForegroundServiceTypes.dataSync.rawValue, 2); + expect(ForegroundServiceTypes.health.rawValue, 3); + expect(ForegroundServiceTypes.location.rawValue, 4); + expect(ForegroundServiceTypes.mediaPlayback.rawValue, 5); + expect(ForegroundServiceTypes.mediaProjection.rawValue, 6); + expect(ForegroundServiceTypes.microphone.rawValue, 7); + expect(ForegroundServiceTypes.phoneCall.rawValue, 8); + expect(ForegroundServiceTypes.remoteMessaging.rawValue, 9); + expect(ForegroundServiceTypes.shortService.rawValue, 10); + expect(ForegroundServiceTypes.specialUse.rawValue, 11); + expect(ForegroundServiceTypes.systemExempted.rawValue, 12); + expect(ForegroundServiceTypes.mediaProcessing.rawValue, 13); + }); + }); +} diff --git a/test/platform_interface_test.dart b/test/platform_interface_test.dart new file mode 100644 index 00000000..4425fecb --- /dev/null +++ b/test/platform_interface_test.dart @@ -0,0 +1,233 @@ +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_method_channel.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('FlutterForegroundTaskPlatform defaults', () { + late _UnimplementedPlatform platform; + + setUp(() { + platform = _UnimplementedPlatform(); + }); + + test('startService throws UnimplementedError', () { + expect( + () => platform.startService( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'c', + channelName: 'n', + ), + iosNotificationOptions: const IOSNotificationOptions(), + foregroundTaskOptions: ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.nothing(), + ), + notificationTitle: 'title', + notificationText: 'text', + ), + throwsUnimplementedError, + ); + }); + + test('restartService throws UnimplementedError', () { + expect( + () => platform.restartService(), + throwsUnimplementedError, + ); + }); + + test('updateService throws UnimplementedError', () { + expect( + () => platform.updateService(), + throwsUnimplementedError, + ); + }); + + test('stopService throws UnimplementedError', () { + expect( + () => platform.stopService(), + throwsUnimplementedError, + ); + }); + + test('isRunningService throws UnimplementedError', () { + expect( + () => platform.isRunningService(), + throwsUnimplementedError, + ); + }); + + test('attachedActivity throws UnimplementedError', () { + expect( + () => platform.attachedActivity, + throwsUnimplementedError, + ); + }); + + test('setTaskHandler throws UnimplementedError', () { + expect( + () => platform.setTaskHandler(_DummyTaskHandler()), + throwsUnimplementedError, + ); + }); + + test('sendDataToTask throws UnimplementedError', () { + expect( + () => platform.sendDataToTask('data'), + throwsUnimplementedError, + ); + }); + + test('minimizeApp throws UnimplementedError', () { + expect( + () => platform.minimizeApp(), + throwsUnimplementedError, + ); + }); + + test('launchApp throws UnimplementedError', () { + expect( + () => platform.launchApp(), + throwsUnimplementedError, + ); + }); + + test('setOnLockScreenVisibility throws UnimplementedError', () { + expect( + () => platform.setOnLockScreenVisibility(true), + throwsUnimplementedError, + ); + }); + + test('isAppOnForeground throws UnimplementedError', () { + expect( + () => platform.isAppOnForeground, + throwsUnimplementedError, + ); + }); + + test('wakeUpScreen throws UnimplementedError', () { + expect( + () => platform.wakeUpScreen(), + throwsUnimplementedError, + ); + }); + + test('isIgnoringBatteryOptimizations throws UnimplementedError', () { + expect( + () => platform.isIgnoringBatteryOptimizations, + throwsUnimplementedError, + ); + }); + + test('openIgnoreBatteryOptimizationSettings throws UnimplementedError', + () { + expect( + () => platform.openIgnoreBatteryOptimizationSettings(), + throwsUnimplementedError, + ); + }); + + test('requestIgnoreBatteryOptimization throws UnimplementedError', () { + expect( + () => platform.requestIgnoreBatteryOptimization(), + throwsUnimplementedError, + ); + }); + + test('canDrawOverlays throws UnimplementedError', () { + expect( + () => platform.canDrawOverlays, + throwsUnimplementedError, + ); + }); + + test('openSystemAlertWindowSettings throws UnimplementedError', () { + expect( + () => platform.openSystemAlertWindowSettings(), + throwsUnimplementedError, + ); + }); + + test('checkNotificationPermission throws UnimplementedError', () { + expect( + () => platform.checkNotificationPermission(), + throwsUnimplementedError, + ); + }); + + test('requestNotificationPermission throws UnimplementedError', () { + expect( + () => platform.requestNotificationPermission(), + throwsUnimplementedError, + ); + }); + + test('canScheduleExactAlarms throws UnimplementedError', () { + expect( + () => platform.canScheduleExactAlarms, + throwsUnimplementedError, + ); + }); + + test('openAlarmsAndRemindersSettings throws UnimplementedError', () { + expect( + () => platform.openAlarmsAndRemindersSettings(), + throwsUnimplementedError, + ); + }); + + test( + 'updateIOSContinuedProcessingTaskProgress throws UnimplementedError', + () { + expect( + () => platform.updateIOSContinuedProcessingTaskProgress( + progress: 0.5, + ), + throwsUnimplementedError, + ); + }); + + test('isIOSContinuedProcessingTaskSupported throws UnimplementedError', + () { + expect( + () => platform.isIOSContinuedProcessingTaskSupported, + throwsUnimplementedError, + ); + }); + }); + + group('FlutterForegroundTaskPlatform instance', () { + test('default instance is MethodChannelFlutterForegroundTask', () { + expect( + FlutterForegroundTaskPlatform.instance, + isA(), + ); + }); + + test('can set and get instance', () { + final original = FlutterForegroundTaskPlatform.instance; + final custom = MethodChannelFlutterForegroundTask(); + FlutterForegroundTaskPlatform.instance = custom; + expect(FlutterForegroundTaskPlatform.instance, same(custom)); + FlutterForegroundTaskPlatform.instance = original; + }); + }); +} + +/// A subclass that does NOT override any methods, so all calls hit +/// the default `throw UnimplementedError()` bodies. +class _UnimplementedPlatform extends FlutterForegroundTaskPlatform {} + +class _DummyTaskHandler extends TaskHandler { + @override + Future onStart(DateTime timestamp, TaskStarter starter) async {} + + @override + void onRepeatEvent(DateTime timestamp) {} + + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async {} +} diff --git a/test/task_handler_test.dart b/test/task_handler_test.dart index 2a19c09b..3cc678bf 100644 --- a/test/task_handler_test.dart +++ b/test/task_handler_test.dart @@ -125,6 +125,47 @@ void main() { }); }); + group('TaskHandler default method bodies', () { + late MinimalTaskHandler minimalHandler; + + setUp(() { + minimalHandler = MinimalTaskHandler(); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mBGChannel, + (MethodCall methodCall) { + return platformChannel.onBackgroundChannel( + methodCall, minimalHandler); + }, + ); + }); + + test('onReceiveData default is a no-op', () async { + await platformChannel.mBGChannel + .invokeMethod(TaskEventMethod.onReceiveData, 'data'); + expect(minimalHandler.log, isEmpty); + }); + + test('onNotificationButtonPressed default is a no-op', () async { + await platformChannel.mBGChannel + .invokeMethod(TaskEventMethod.onNotificationButtonPressed, 'btn'); + expect(minimalHandler.log, isEmpty); + }); + + test('onNotificationPressed default is a no-op', () async { + await platformChannel.mBGChannel + .invokeMethod(TaskEventMethod.onNotificationPressed); + expect(minimalHandler.log, isEmpty); + }); + + test('onNotificationDismissed default is a no-op', () async { + await platformChannel.mBGChannel + .invokeMethod(TaskEventMethod.onNotificationDismissed); + expect(minimalHandler.log, isEmpty); + }); + }); + group('CommunicationPort', () { test('initCommunicationPort', () { FlutterForegroundTask.initCommunicationPort(); @@ -282,6 +323,27 @@ class TaskEventMethod { static const String onNotificationDismissed = 'onNotificationDismissed'; } +/// Only implements required abstract methods; optional callbacks use the +/// base class default (no-op) bodies so we can verify they are covered. +class MinimalTaskHandler extends TaskHandler { + final List log = []; + + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + log.add('onStart'); + } + + @override + void onRepeatEvent(DateTime timestamp) { + log.add('onRepeatEvent'); + } + + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + log.add('onDestroy'); + } +} + class TestTaskHandler extends TaskHandler { final List log = []; diff --git a/test/with_foreground_task_test.dart b/test/with_foreground_task_test.dart new file mode 100644 index 00000000..98a06144 --- /dev/null +++ b/test/with_foreground_task_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_method_channel.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform/platform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MethodChannelFlutterForegroundTask platformChannel; + bool serviceRunning = false; + int minimizeAppCallCount = 0; + + setUp(() { + platformChannel = MethodChannelFlutterForegroundTask(); + platformChannel.platform = + FakePlatform(operatingSystem: Platform.android); + FlutterForegroundTaskPlatform.instance = platformChannel; + FlutterForegroundTask.resetStatic(); + serviceRunning = false; + minimizeAppCallCount = 0; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mMDChannel, + (MethodCall methodCall) async { + if (methodCall.method == 'isRunningService') { + return serviceRunning; + } + if (methodCall.method == 'minimizeApp') { + minimizeAppCallCount++; + return null; + } + return null; + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformChannel.mMDChannel, null); + }); + + group('WithForegroundTask', () { + testWidgets('renders child widget', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: WithForegroundTask( + child: Scaffold(body: Text('Hello')), + ), + ), + ); + + expect(find.text('Hello'), findsOneWidget); + }); + + testWidgets( + 'back button minimizes app when service is running and cannot pop', + (tester) async { + serviceRunning = true; + + await tester.pumpWidget( + const MaterialApp( + home: WithForegroundTask( + child: Scaffold(body: Text('Root')), + ), + ), + ); + + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + + expect(minimizeAppCallCount, 1); + expect(find.text('Root'), findsOneWidget); + }, + ); + + testWidgets( + 'back button exits app when service is not running and cannot pop', + (tester) async { + serviceRunning = false; + + await tester.pumpWidget( + const MaterialApp( + home: WithForegroundTask( + child: Scaffold(body: Text('Root')), + ), + ), + ); + + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + + // minimizeApp should not have been called. + expect(minimizeAppCallCount, 0); + }, + ); + + testWidgets( + 'back button pops normally when navigator has history', + (tester) async { + serviceRunning = true; + + await tester.pumpWidget( + MaterialApp( + home: WithForegroundTask( + child: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const Scaffold( + body: Text('Page 2'), + ), + ), + ); + }, + child: const Text('Go'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Go')); + await tester.pumpAndSettle(); + expect(find.text('Page 2'), findsOneWidget); + + // canPop is true so the system handles the pop directly. + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + + expect(minimizeAppCallCount, 0); + expect(find.text('Go'), findsOneWidget); + }, + ); + }); +}