Skip to content

Commit

Permalink
feat(EC-515): Handle Audio Recorder Leakage when started but not stop…
Browse files Browse the repository at this point in the history
…ped (#33)

* feat(EC-515): Handle Media Recorder Leakage when started but not stopped

* test(EC-515): Media Recorder Leakage test implementation

* feat(EC-515): Handle Audio Recorder Leakage when started but not stopped

* feat(EC-515): Add missing License

* feat(EC-515): Solve cognitive complexity sonar issue

---------

Co-authored-by: GwendalSIWIOREK <gwendal.siwiorek@exalt-it.com>
  • Loading branch information
ExalTomiche and gsiwiorek-exalt authored May 30, 2024
1 parent adfea75 commit e7cb7c5
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* ecoCode iOS plugin - Help the earth, adopt this green plugin for your applications
* Copyright © 2023 green-code-initiative (https://www.ecocode.io/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.ecocode.ios.swift.checks.sobriety;

import io.ecocode.ios.swift.SwiftRuleCheck;
import io.ecocode.ios.swift.antlr.generated.Swift5Parser;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNodeImpl;
import org.sonar.check.Rule;

@Rule(key = "EC515")
public class AudioRecorderLeakCheck extends SwiftRuleCheck {
private static final String DEFAULT_ISSUE_MESSAGE = "Any audio recording started should be stopped.";
Swift5Parser.ExpressionContext id = null;
private boolean audioRecorderStarted = false;
private boolean audioRecorderStopped = false;
private boolean importExist = false;


@Override
public void apply(ParseTree tree) {
if (!importExist && tree instanceof Swift5Parser.ExpressionContext && tree.getText().contains("AVAudioRecorder")) {
importExist = true;
}

if (importExist) {
findStartedButNotStoppedAudioRecord(tree);
}
}

private void findStartedButNotStoppedAudioRecord(ParseTree tree) {
if (tree instanceof Swift5Parser.ExpressionContext && tree.getText().contains("record()")) {
id = (Swift5Parser.ExpressionContext) tree;
audioRecorderStarted = true;
}

if (tree instanceof Swift5Parser.ExpressionContext && (tree.getText().contains("stop()"))) {
audioRecorderStopped = true;
}

if (tree instanceof TerminalNodeImpl && tree.getText().equals("<EOF>")) {
if (audioRecorderStarted && !audioRecorderStopped) {
this.recordIssue(id.getStart().getStartIndex(), DEFAULT_ISSUE_MESSAGE);
}
audioRecorderStarted = false;
audioRecorderStopped = false;
importExist = false;
}
}
}
1 change: 1 addition & 0 deletions swift-lang/src/main/resources/ecocode_swift_profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"EC512",
"EC513",
"EC514",
"EC515",
"EC519",
"EC520",
"EC522",
Expand Down
58 changes: 58 additions & 0 deletions swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<p>Creation of an <code>AVAudioRecorder</code> object is used to record audio. This class has methods to stop recording
and release resources.

In addition to unnecessary resources (such as memory and instances of codecs) being held, failure to properly stop
and release these objects if they are no longer needed may also lead to continuous battery consumption for mobile
devices.</p>
<h2>Noncompliant Code Example</h2>
<pre>
import AVFoundation

var audioRecorder: AVAudioRecorder?

func startRecording() {
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]

do {
audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings)
audioRecorder?.record()
} catch {
// Handle error
}
}
</pre>
<br>
<h2>Compliant Code Example</h2>
<pre>
import AVFoundation

var audioRecorder: AVAudioRecorder?

func startRecording() {
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]

do {
audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings)
audioRecorder?.record()
} catch {
// Handle error
}
}

func stopRecording() {
if let recorder = audioRecorder, recorder.isRecording {
recorder.stop()
audioRecorder = nil
}
}
</pre>
19 changes: 19 additions & 0 deletions swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"key": "EC515",
"title": "Leakage: Audio and Video Recorder Leak",
"defaultSeverity": "Major",
"description": "Creation of an AVAudioRecorder object is used to record audio. This class has methods to stop recording and release resources. In addition to unnecessary resources (such as memory and instances of codecs) being held, failure to properly stop and release these objects if they are no longer needed may also lead to continuous battery consumption for mobile devices.",
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "7min"
},
"tags": [
"sobriety",
"environment",
"ecocode",
"eco-design"
],
"type": "CODE_SMELL"
}

Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void testMetadata() {

@Test
public void testRegisteredRules() {
assertThat(repository.rules()).hasSize(13);
assertThat(repository.rules()).hasSize(14);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* ecoCode iOS plugin - Help the earth, adopt this green plugin for your applications
* Copyright © 2023 green-code-initiative (https://www.ecocode.io/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.ecocode.ios.swift.checks.sobriety;

import io.ecocode.ios.swift.checks.CheckTestHelper;
import org.junit.Test;
import org.sonar.api.batch.sensor.internal.SensorContextTester;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.IssueLocation;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

public class AudioRecorderLeakCheckTest {
private static final String TEST_AUDIO_CASE_MISSING_RELEASE_CALL = "checks/sobriety/AudioRecorderLeak_trigger.swift";
private static final String TEST_AUDIO_CASE_COMPLIANT = "checks/sobriety/AudioRecorderLeak_no_trigger.swift";

private static final String TESTED_RULE_ID = "EC515";
private static final String TEST_REPOSITORY = "ecoCode-swift";

@Test
public void audioLeakCheck_missing_release_trigger() {
SensorContextTester context = CheckTestHelper.analyzeTestFile(TEST_AUDIO_CASE_MISSING_RELEASE_CALL);
assertThat(context.allIssues()).hasSize(1);
Optional<Issue> issue = context.allIssues().stream().findFirst();
issue.ifPresent(i -> {
assertThat(i.ruleKey().rule()).isEqualTo(TESTED_RULE_ID);
assertThat(i.ruleKey().repository()).isEqualTo(TEST_REPOSITORY);
IssueLocation location = i.primaryLocation();
assertThat(location.textRange().start().line()).isEqualTo(15);
});
}

@Test
public void audioLeakCheck_no_trigger() {
SensorContextTester context = CheckTestHelper.analyzeTestFile(TEST_AUDIO_CASE_COMPLIANT);
assertThat(context.allIssues()).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import AVFoundation

var audioRecorder: AVAudioRecorder?

func startRecording() {
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]

do {
audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings)
audioRecorder?.record()
} catch {
// Handle error
}
}

func stopRecording() {
if let recorder = audioRecorder, recorder.isRecording {
recorder.stop()
audioRecorder = nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import AVFoundation

var audioRecorder: AVAudioRecorder?

func startRecording() {
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]

do {
audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings)
audioRecorder?.record()
} catch {
// Handle error
}
}

0 comments on commit e7cb7c5

Please sign in to comment.