Signal-iOS/SignalUI/ViewModels/VoiceMessageModel.swift
Evan Hahn 370ff654e7
Change license to AGPL
Change license to AGPL

This commit:

- Updates the `LICENSE` file

- Start every file with something like:

      // Copyright YEAR_FIRST_PUBLISHED Signal Messenger, LLC
      // SPDX-License-Identifier: AGPL-3.0-only

---

First, I removed existing license headers with this Ruby 3.1.2 script:

    require 'set'

    EXTENSIONS_TO_CHECK = Set['.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift']

    same = 0
    different = 0

    all_files = `git ls-files`.lines.map { |line| line.strip }
    all_files.each do |relative_path|
      if relative_path == 'Pods'
        next
      end

      unless EXTENSIONS_TO_CHECK.include? File.extname(relative_path)
        next
      end

      path = File.expand_path(relative_path)

      contents = File.read(path)
      new_contents = contents.sub(/\/\/\n\/\/  Copyright .*\n\/\/\n\n/, '')

      if contents == new_contents
        same += 1
      else
        different += 1
      end

      File.write(path, new_contents)
    end

    puts "updated #{different} file(s), left #{same} untouched"

I'm sure this script could be improved, but it worked well enough.

Then, I created `Scripts/lint/lint-license-headers` and ran it to auto-
fix a lot of files. This changed the mode of some files, but I think
that's actually desirable. For example,
`SignalServiceKit/src/Util/AppContext.m` previously had a mode of
`0755/-rwxr-xr-x`, and it's now `0644/-rw-r--r--`.

Then I fixed some stragglers and updated the precommit script.

See [a similar change in the Desktop app][0].

[0]: 8bfaf598af
2022-10-13 08:25:37 -05:00

187 lines
6.2 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import AVFoundation
import CoreServices
import SignalMessaging
@objc
public class VoiceMessageModel: NSObject {
public let threadUniqueId: String
private static var draftVoiceMessageDirectory: URL { VoiceMessageModels.draftVoiceMessageDirectory }
@objc
public init(thread: TSThread) {
self.threadUniqueId = thread.uniqueId
}
// MARK: -
private static let audioExtension = "m4a"
private static let audioUTI: String = kUTTypeMPEG4Audio as String
@objc
public func prepareForSending() throws -> SignalAttachment {
guard !isRecording else {
throw OWSAssertionError("Can't send while actively recording")
}
guard OWSFileSystem.fileOrFolderExists(url: audioFile) else {
throw OWSAssertionError("Missing audio file")
}
let temporaryDirectory = URL(fileURLWithPath: OWSTemporaryDirectory(), isDirectory: true)
let temporaryAudioFile = URL(fileURLWithPath: audioFile.lastPathComponent, relativeTo: temporaryDirectory)
try FileManager.default.copyItem(at: audioFile, to: temporaryAudioFile)
let dataSource = try DataSourcePath.dataSource(with: temporaryAudioFile, shouldDeleteOnDeallocation: true)
dataSource.sourceFilename = outputFileName(at: Date())
let attachment = SignalAttachment.voiceMessageAttachment(dataSource: dataSource, dataUTI: Self.audioUTI)
guard !attachment.hasError else {
throw OWSAssertionError("Failed to create voice message attachment: \(attachment.errorName ?? "Unknown Error")")
}
return attachment
}
// MARK: -
@objc
public func saveDraft(transaction: SDSAnyWriteTransaction) {
VoiceMessageModels.saveDraft(threadUniqueId: threadUniqueId, transaction: transaction)
}
@objc
public func clearDraft(transaction: SDSAnyWriteTransaction) {
VoiceMessageModels.clearDraft(for: threadUniqueId, transaction: transaction)
}
// MARK: -
private var directory: URL {
let directory = VoiceMessageModels.directory(for: threadUniqueId)
OWSFileSystem.ensureDirectoryExists(directory.path)
return directory
}
public lazy var audioWaveform: AudioWaveform? =
AudioWaveformManager.audioWaveform(forAudioPath: audioFile.path, waveformPath: waveformFile.path)
public lazy var audioPlayer: OWSAudioPlayer =
.init(mediaUrl: audioFile, audioBehavior: .audioMessagePlayback)
private var audioFile: URL { URL(fileURLWithPath: "voice-memo.\(Self.audioExtension)", relativeTo: directory) }
private var waveformFile: URL { URL(fileURLWithPath: "waveform.dat", relativeTo: directory) }
private func outputFileName(at date: Date) -> String {
String(
format: "%@ %@.%@",
OWSLocalizedString("VOICE_MESSAGE_FILE_NAME", comment: "Filename for voice messages."),
DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .short),
Self.audioExtension
)
}
// MARK: -
@objc
public var isRecording: Bool { audioRecorder?.isRecording ?? false }
public lazy var duration: TimeInterval? = {
guard OWSFileSystem.fileOrFolderExists(url: audioFile) else { return nil }
audioPlayer.setupAudioPlayer()
return audioPlayer.duration
}()
private var audioRecorder: AVAudioRecorder? {
didSet {
guard oldValue != audioRecorder else { return }
if let oldValue = oldValue {
DeviceSleepManager.shared.removeBlock(blockObject: oldValue)
}
if let audioRecorder = audioRecorder {
DeviceSleepManager.shared.addBlock(blockObject: audioRecorder)
}
}
}
private lazy var audioActivity = AudioActivity(audioDescription: "Voice Message Recording", behavior: .playAndRecord)
public func startRecording() throws {
AssertIsOnMainThread()
guard !isRecording else {
throw OWSAssertionError("Attempted to start recording while recording is in progress")
}
OWSFileSystem.deleteContents(ofDirectory: directory.path)
guard audioSession.startAudioActivity(audioActivity) else {
throw OWSAssertionError("Couldn't configure audio session")
}
let audioRecorder: AVAudioRecorder
do {
audioRecorder = try AVAudioRecorder(
url: audioFile,
settings: [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100,
AVNumberOfChannelsKey: 2,
AVEncoderBitRateKey: 128 * 1024
]
)
self.audioRecorder = audioRecorder
} catch {
throw OWSAssertionError("Couldn't create audioRecorder: \(error)")
}
audioRecorder.isMeteringEnabled = true
guard audioRecorder.prepareToRecord() else {
throw OWSAssertionError("audioRecorder couldn't prepareToRecord.")
}
guard audioRecorder.record() else {
throw OWSAssertionError("audioRecorder couldn't record.")
}
}
public func stopRecording() {
AssertIsOnMainThread()
guard let audioRecorder = audioRecorder else { return }
self.duration = audioRecorder.currentTime
audioRecorder.stop()
self.audioRecorder = nil
// This is expensive. We can safely do it in the background.
DispatchQueue.sharedUserInteractive.async {
self.audioSession.endAudioActivity(self.audioActivity)
}
}
public func stopRecordingAsync() {
AssertIsOnMainThread()
guard let audioRecorder = audioRecorder else { return }
self.audioRecorder = nil
self.duration = audioRecorder.currentTime
// This is expensive. We can safely do it in the background
// if we're not relying on the recorded audio (e.g. we canceled)
DispatchQueue.sharedUserInteractive.async {
audioRecorder.stop()
self.audioSession.endAudioActivity(self.audioActivity)
}
}
}