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 - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| Directory |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| lib/ | +
+ |
+ 97.1 % | +384 | +373 | +|
| lib/models/ | +
+ |
+ 99.4 % | +157 | +156 | +|
| lib/errors/ | +
+ |
+ 100.0 % | +12 | +12 | +|
| lib/ui/ | +
+ |
+ 100.0 % | +12 | +12 | +|
| lib/utils/ | +
+ |
+ 100.0 % | +12 | +12 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| Directory |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| lib/ | +
+ |
+ 97.1 % | +384 | +373 | +|
| lib/errors/ | +
+ |
+ 100.0 % | +12 | +12 | +|
| lib/models/ | +
+ |
+ 99.4 % | +157 | +156 | +|
| lib/ui/ | +
+ |
+ 100.0 % | +12 | +12 | +|
| lib/utils/ | +
+ |
+ 100.0 % | +12 | +12 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| service_already_started_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| service_not_initialized_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| service_not_started_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| service_timeout_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| service_already_started_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| service_not_initialized_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| service_not_started_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| service_timeout_exception.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| flutter_foreground_task_method_channel.dart | +
+ |
+ 93.3 % | +105 | +98 | +|
| flutter_foreground_task.dart | +
+ |
+ 97.9 % | +97 | +95 | +|
| flutter_foreground_task_controller.dart | +
+ |
+ 98.4 % | +122 | +120 | +|
| task_handler.dart | +
+ |
+ 100.0 % | +5 | +5 | +|
| flutter_foreground_task_platform_interface.dart | +
+ |
+ 100.0 % | +55 | +55 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| flutter_foreground_task.dart | +
+ |
+ 97.9 % | +97 | +95 | +|
| flutter_foreground_task_controller.dart | +
+ |
+ 98.4 % | +122 | +120 | +|
| flutter_foreground_task_method_channel.dart | +
+ |
+ 93.3 % | +105 | +98 | +|
| flutter_foreground_task_platform_interface.dart | +
+ |
+ 100.0 % | +55 | +55 | +|
| task_handler.dart | +
+ |
+ 100.0 % | +5 | +5 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| notification_options.dart | +
+ |
+ 98.4 % | +61 | +60 | +|
| foreground_service_types.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| notification_channel_importance.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| notification_priority.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| notification_visibility.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| notification_permission.dart | +
+ |
+ 100.0 % | +2 | +2 | +|
| service_request_result.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| notification_icon.dart | +
+ |
+ 100.0 % | +10 | +10 | +|
| foreground_task_event_action.dart | +
+ |
+ 100.0 % | +11 | +11 | +|
| notification_button.dart | +
+ |
+ 100.0 % | +13 | +13 | +|
| foreground_task_options.dart | +
+ |
+ 100.0 % | +19 | +19 | +|
| service_options.dart | +
+ |
+ 100.0 % | +34 | +34 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| foreground_service_types.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| foreground_task_event_action.dart | +
+ |
+ 100.0 % | +11 | +11 | +|
| foreground_task_options.dart | +
+ |
+ 100.0 % | +19 | +19 | +|
| notification_button.dart | +
+ |
+ 100.0 % | +13 | +13 | +|
| notification_channel_importance.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| notification_icon.dart | +
+ |
+ 100.0 % | +10 | +10 | +|
| notification_options.dart | +
+ |
+ 98.4 % | +61 | +60 | +|
| notification_permission.dart | +
+ |
+ 100.0 % | +2 | +2 | +|
| notification_priority.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| notification_visibility.dart | +
+ |
+ 100.0 % | +1 | +1 | +|
| service_options.dart | +
+ |
+ 100.0 % | +34 | +34 | +|
| service_request_result.dart | +
+ |
+ 100.0 % | +3 | +3 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| with_foreground_task.dart | +
+ |
+ 100.0 % | +12 | +12 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| color_extension.dart | +
+ |
+ 100.0 % | +4 | +4 | +|
| utility.dart | +
+ |
+ 100.0 % | +8 | +8 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| color_extension.dart | +
+ |
+ 100.0 % | +4 | +4 | +|
| utility.dart | +
+ |
+ 100.0 % | +8 | +8 | +|
| Generated by: LCOV version 2.3.1-1 |
| LCOV - code coverage report | ||||||||||||||||||
+
|
+ ||||||||||||||||||
+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 |