Compare commits

..

No commits in common. "f697248d0c2825330ac6f2063465efd66fdeac29" and "383fbab7d9ae4d82c13456ded2eb90d6d98f04d3" have entirely different histories.

26 changed files with 183 additions and 394 deletions

View file

@ -2,17 +2,20 @@ package com.vodemn.lightmeter
import android.os.Bundle
import android.view.KeyEvent
import android.view.WindowManager
import androidx.core.view.WindowCompat
import com.vodemn.lightmeter.PlatformChannels.CaffeinePlatformChannel
import com.vodemn.lightmeter.PlatformChannels.CameraInfoPlatformChannel
import com.vodemn.lightmeter.PlatformChannels.VolumePlatformChannel
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val caffeinePlatformChannel = CaffeinePlatformChannel()
private val cameraInfoPlatformChannel = CameraInfoPlatformChannel()
private val volumePlatformChannel = VolumePlatformChannel()
private lateinit var keepScreenOnChannel: MethodChannel
private lateinit var volumeHandlingChannel: MethodChannel
private lateinit var volumeEventChannel: EventChannel
private var volumeEventsEmitter: EventSink? = null
private var handleVolume = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -21,24 +24,72 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger
caffeinePlatformChannel.onAttachedToEngine(binaryMessenger, window)
cameraInfoPlatformChannel.onAttachedToEngine(binaryMessenger, context)
volumePlatformChannel.onAttachedToEngine(binaryMessenger)
keepScreenOnChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.vodemn.lightmeter/keepScreenOn"
)
keepScreenOnChannel.setMethodCallHandler { call, result ->
when (call.method) {
"isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0)
"setKeepScreenOn" -> {
if (call.arguments !is Boolean) {
result.error("invalid args", "Argument should be of type Bool for 'setKeepScreenOn' call", null)
} else {
if (call.arguments as Boolean) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
result.success(true)
}
}
else -> result.notImplemented()
}
}
volumeHandlingChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.vodemn.lightmeter/volumeHandling"
)
volumeHandlingChannel.setMethodCallHandler { call, result ->
when (call.method) {
"setVolumeHandling" -> {
handleVolume = call.arguments as Boolean
result.success(handleVolume)
}
else -> result.notImplemented()
}
}
volumeEventChannel = EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.vodemn.lightmeter/volumeEvents"
)
volumeEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(listener: Any?, eventSink: EventSink) {
volumeEventsEmitter = eventSink
}
override fun onCancel(listener: Any?) {
volumeEventsEmitter = null
}
})
}
override fun onDestroy() {
caffeinePlatformChannel.onDestroy()
cameraInfoPlatformChannel.onDestroy()
volumePlatformChannel.onDestroy()
keepScreenOnChannel.setMethodCallHandler(null)
volumeHandlingChannel.setMethodCallHandler(null)
volumeEventChannel.setStreamHandler(null)
super.onDestroy()
}
override fun onKeyDown(code: Int, event: KeyEvent): Boolean {
return if (volumePlatformChannel.onKeyDown(code, event)) {
true
} else {
super.onKeyDown(code, event)
return when (val keyCode: Int = event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (handleVolume) {
volumeEventsEmitter?.success(keyCode)
true
} else {
super.onKeyDown(code, event)
}
}
else -> super.onKeyDown(code, event)
}
}
}

View file

@ -1,42 +0,0 @@
package com.vodemn.lightmeter.PlatformChannels
import android.view.Window
import android.view.WindowManager
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
/** CaffeinePlatformChannel */
class CaffeinePlatformChannel {
private lateinit var channel: MethodChannel
fun onAttachedToEngine(binaryMessenger: BinaryMessenger, window: Window) {
channel = MethodChannel(
binaryMessenger,
"com.vodemn.lightmeter.CaffeinePlatformChannel.MethodChannel"
)
channel.setMethodCallHandler { call, result ->
when (call.method) {
"isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0)
"setKeepScreenOn" -> {
if (call.arguments !is Boolean) {
result.error(
"invalid args",
"Argument should be of type Bool for 'setKeepScreenOn' call",
null
)
} else {
if (call.arguments as Boolean) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
result.success(true)
}
}
else -> result.notImplemented()
}
}
}
fun onDestroy() {
channel.setMethodCallHandler(null)
}
}

View file

@ -1,59 +0,0 @@
package com.vodemn.lightmeter.PlatformChannels
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata.LENS_FACING_BACK
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import kotlin.math.pow
import kotlin.math.sqrt
/** CameraInfoPlatformChannel */
class CameraInfoPlatformChannel {
private lateinit var channel: MethodChannel
private lateinit var cameraManager: CameraManager
private var mainCameraEfl: Double? = null
fun onAttachedToEngine(binaryMessenger: BinaryMessenger, context: Context) {
channel = MethodChannel(
binaryMessenger,
"com.vodemn.lightmeter.CameraInfoPlatformChannel.MethodChannel"
)
cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
channel.setMethodCallHandler { call, result ->
when (call.method) {
"mainCameraEfl" -> {
mainCameraEfl = mainCameraEfl ?: getMainCameraFocalLength35mm()
result.success(mainCameraEfl)
}
else -> result.notImplemented()
}
}
}
fun onDestroy() {
channel.setMethodCallHandler(null)
}
private fun getMainCameraFocalLength35mm(): Double? {
return cameraManager.cameraIdList.map {
cameraManager.getCameraCharacteristics(it)
}.first {
it.get(CameraCharacteristics.LENS_FACING) == LENS_FACING_BACK
}.focalLength35mm()
}
private fun CameraCharacteristics.focalLength35mm(): Double? {
val defaultFocalLength =
get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)?.first()
val sensorSize = get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
return if (defaultFocalLength != null && sensorSize != null) {
// https://en.wikipedia.org/wiki/35_mm_equivalent_focal_length#Conversions
43.27 * defaultFocalLength / sqrt(sensorSize.height.pow(2) + sensorSize.width.pow(2))
} else {
null
}
}
}

View file

@ -1,66 +0,0 @@
package com.vodemn.lightmeter.PlatformChannels
import android.view.KeyEvent
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import io.flutter.plugin.common.MethodChannel
/** VolumePlatformChannel */
class VolumePlatformChannel {
private lateinit var volumeMethodChannel: MethodChannel
private lateinit var volumeEventChannel: EventChannel
private var volumeEventsEmitter: EventSink? = null
private var handleVolume = false
fun onAttachedToEngine(binaryMessenger: BinaryMessenger) {
volumeMethodChannel = MethodChannel(
binaryMessenger,
"com.vodemn.lightmeter.VolumePlatformChannel.MethodChannel"
)
volumeMethodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"setVolumeHandling" -> {
handleVolume = call.arguments as Boolean
result.success(handleVolume)
}
else -> result.notImplemented()
}
}
volumeEventChannel = EventChannel(
binaryMessenger,
"com.vodemn.lightmeter.VolumePlatformChannel.EventChannel"
)
volumeEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(listener: Any?, eventSink: EventSink) {
volumeEventsEmitter = eventSink
}
override fun onCancel(listener: Any?) {
volumeEventsEmitter = null
}
})
}
fun onDestroy() {
volumeMethodChannel.setMethodCallHandler(null)
volumeEventChannel.setStreamHandler(null)
}
fun onKeyDown(code: Int, event: KeyEvent): Boolean {
return when (val keyCode: Int = event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (handleVolume) {
volumeEventsEmitter?.success(keyCode)
true
} else {
false
}
}
else -> false
}
}
}

View file

@ -7,8 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
001BDA042DD1252B00122957 /* CameraInfoPlatformChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */; };
0035E99F2DD0B07C0053508B /* CaffeinePlatformChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
5A1AF02F1E4619A6E479AA8B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C991C89D5763E562E77E475E /* Pods_Runner.framework */; };
@ -33,8 +31,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraInfoPlatformChannel.swift; sourceTree = "<group>"; };
0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaffeinePlatformChannel.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
@ -73,15 +69,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0035E99D2DD0B05F0053508B /* PlatformChannels */ = {
isa = PBXGroup;
children = (
0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */,
001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */,
);
path = PlatformChannels;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@ -123,7 +110,6 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
0035E99D2DD0B05F0053508B /* PlatformChannels */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
@ -170,7 +156,7 @@
45F53C083F2EA48EF231DA16 /* [CP] Embed Pods Frameworks */,
FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */,
08127035D2CDEEEBA66FCDBB /* [CP] Copy Pods Resources */,
58ED09C740BAF94D922BAC2C /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
051083B0AECD5071E5FA2A6F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
);
buildRules = (
);
@ -230,6 +216,24 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
051083B0AECD5071E5FA2A6F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\"\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n";
};
08127035D2CDEEEBA66FCDBB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -302,24 +306,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
58ED09C740BAF94D922BAC2C /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 8;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 1;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\"\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -333,7 +319,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */ = {
isa = PBXShellScriptBuildPhase;
@ -366,8 +352,6 @@
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
001BDA042DD1252B00122957 /* CameraInfoPlatformChannel.swift in Sources */,
0035E99F2DD0B07C0053508B /* CaffeinePlatformChannel.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -619,9 +603,11 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 489Z6UQMGN;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
@ -633,6 +619,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
PRODUCT_NAME = Lightmeter;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Lightmeter Distribution";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";

View file

@ -8,8 +8,25 @@ import Flutter
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let caffeinePlatformChannel = CaffeinePlatformChannel(binaryMessenger: controller.binaryMessenger)
let cameraInfoPlatformChannel = CameraInfoPlatformChannel(binaryMessenger: controller.binaryMessenger)
let keepScreenOnChannel = FlutterMethodChannel(name: "com.vodemn.lightmeter/keepScreenOn",
binaryMessenger: controller.binaryMessenger)
keepScreenOnChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "isKeepScreenOn":
result(UIApplication.shared.isIdleTimerDisabled)
case "setKeepScreenOn":
guard let keepOn = call.arguments as? Bool else {
result(FlutterError(code: "invalid arguments", message: "Argument should be of type Bool for 'setKeepScreenOn' call", details: nil))
return
}
UIApplication.shared.isIdleTimerDisabled = keepOn
result(true)
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

View file

@ -1,35 +0,0 @@
//
// CaffeinePlatformChannel.swift
// Runner
//
// Created by Vadim Turko on 2025-05-11.
//
import Flutter
public class CaffeinePlatformChannel: NSObject {
let methodChannel: FlutterMethodChannel
init(binaryMessenger: FlutterBinaryMessenger) {
self.methodChannel = FlutterMethodChannel(
name: "com.vodemn.lightmeter.CaffeinePlatformChannel.MethodChannel",
binaryMessenger: binaryMessenger
)
super.init()
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "isKeepScreenOn":
result(UIApplication.shared.isIdleTimerDisabled)
case "setKeepScreenOn":
guard let keepOn = call.arguments as? Bool else {
result(FlutterError(code: "invalid arguments", message: "Argument should be of type Bool for 'setKeepScreenOn' call", details: nil))
return
}
UIApplication.shared.isIdleTimerDisabled = keepOn
result(true)
default:
result(FlutterMethodNotImplemented)
}
})
}
}

View file

@ -1,45 +0,0 @@
//
// CameraInfoPlatformChannel.swift
// Runner
//
// Created by Vadim Turko on 2025-05-11.
//
import AVFoundation
import Flutter
public class CameraInfoPlatformChannel: NSObject {
let methodChannel: FlutterMethodChannel
init(binaryMessenger: FlutterBinaryMessenger) {
self.methodChannel = FlutterMethodChannel(
name: "com.vodemn.lightmeter.CameraInfoPlatformChannel.MethodChannel",
binaryMessenger: binaryMessenger
)
super.init()
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "mainCameraEfl":
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
result(self.get35mmEquivalentFocalLength(format: device.activeFormat))
} else {
result(nil)
}
default:
result(FlutterMethodNotImplemented)
}
})
}
func get35mmEquivalentFocalLength(format : AVCaptureDevice.Format) -> Float {
// get reported field of view. Documentation says this is the horizontal field of view
var fov = format.videoFieldOfView
// convert to radians
fov *= Float.pi/180.0
// angle and opposite of right angle triangle are half the fov and half the width of
// 35mm film (ie 18mm). The adjacent value of the right angle triangle is the equivalent
// focal length. Using some right angle triangle math you can work out focal length
let focalLen = 18 / tan(fov/2)
return focalLen
}
}

View file

@ -5,7 +5,6 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:lightmeter/data/analytics/analytics.dart';
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/camera_info_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
@ -100,13 +99,11 @@ class _ApplicationWrapperState extends State<ApplicationWrapper> {
await Future.wait([
SharedPreferences.getInstance(),
const LightSensorService(LocalPlatform()).hasSensor(),
const CameraInfoService().mainCameraEfl(),
remoteConfigService.activeAndFetchFeatures(),
equipmentProfilesStorageService.init(),
filmsStorageService.init(),
]).then((value) {
userPreferencesService = UserPreferencesService((value[0] as SharedPreferences?)!)
..cameraFocalLength = value[2] as int?;
userPreferencesService = UserPreferencesService((value[0] as SharedPreferences?)!);
hasLightSensor = value[1] as bool? ?? false;
});
}

View file

@ -1,17 +1,15 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class CaffeineService {
@visibleForTesting
static const caffeineMethodChannel = MethodChannel("com.vodemn.lightmeter.CaffeinePlatformChannel.MethodChannel");
static const _methodChannel = MethodChannel("com.vodemn.lightmeter/keepScreenOn");
const CaffeineService();
Future<bool> isKeepScreenOn() async {
return caffeineMethodChannel.invokeMethod<bool>("isKeepScreenOn").then((value) => value!);
return _methodChannel.invokeMethod<bool>("isKeepScreenOn").then((value) => value!);
}
Future<bool> keepScreenOn(bool keep) async {
return caffeineMethodChannel.invokeMethod<bool>("setKeepScreenOn", keep).then((value) => value!);
return _methodChannel.invokeMethod<bool>("setKeepScreenOn", keep).then((value) => value!);
}
}

View file

@ -1,16 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class CameraInfoService {
@visibleForTesting
static const cameraInfoPlatformChannel = MethodChannel(
"com.vodemn.lightmeter.CameraInfoPlatformChannel.MethodChannel",
);
const CameraInfoService();
Future<int?> mainCameraEfl() async {
final focalLength = await cameraInfoPlatformChannel.invokeMethod<double?>('mainCameraEfl');
return focalLength?.round();
}
}

View file

@ -6,10 +6,10 @@ class VolumeEventsService {
final LocalPlatform _localPlatform;
@visibleForTesting
static const volumeMethodChannel = MethodChannel("com.vodemn.lightmeter.VolumePlatformChannel.MethodChannel");
static const volumeHandlingChannel = MethodChannel("com.vodemn.lightmeter/volumeHandling");
@visibleForTesting
static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter.VolumePlatformChannel.EventChannel");
static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter/volumeEvents");
const VolumeEventsService(this._localPlatform);
@ -19,7 +19,9 @@ class VolumeEventsService {
if (!_localPlatform.isAndroid) {
return false;
}
return volumeMethodChannel.invokeMethod<bool>("setVolumeHandling", enableHandling).then((value) => value!);
return volumeHandlingChannel
.invokeMethod<bool>("setVolumeHandling", enableHandling)
.then((value) => value!);
}
/// Emits new events on
@ -30,6 +32,9 @@ class VolumeEventsService {
if (!_localPlatform.isAndroid) {
return const Stream.empty();
}
return volumeEventsChannel.receiveBroadcastStream().cast<int>().where((event) => event == 24 || event == 25);
return volumeEventsChannel
.receiveBroadcastStream()
.cast<int>()
.where((event) => event == 24 || event == 25);
}
}

View file

@ -45,8 +45,6 @@
"cameraFeatureSpotMeteringHint": "Halte die Kameraansicht gedrückt um den Messpunkt zu entfernen",
"cameraFeatureHistogram": "Histogramm",
"cameraFeatureHistogramHint": "Verwendung des Histogramms kann den Batterieverbrauch erhöhen",
"cameraFeaturesShowFocalLength": "Brennweite anzeigen",
"cameraFeaturesShowFocalLengthHint": "Zeigt die 35mm-äquivalente Brennweite statt des Zoomfaktors an",
"film": "Film",
"filmPush": "Film (push)",
"filmPull": "Film (pull)",

View file

@ -46,7 +46,7 @@
"cameraFeatureHistogram": "Histogram",
"cameraFeatureHistogramHint": "Enabling histogram can encrease battery drain",
"cameraFeaturesShowFocalLength": "Show Focal Length",
"cameraFeaturesShowFocalLengthHint": "Displays 35mm equivalent focal length instead of zoom factor",
"cameraFeaturesShowFocalLengthHint": "Focal length will be determined after the first photo is taken",
"film": "Film",
"filmPush": "Film (push)",
"filmPull": "Film (pull)",

View file

@ -46,7 +46,7 @@
"cameraFeatureHistogram": "Histogramme",
"cameraFeatureHistogramHint": "L'activation de l'histogramme peut augmenter la consommation de la batterie",
"cameraFeaturesShowFocalLength": "Afficher la focale",
"cameraFeaturesShowFocalLengthHint": "Affiche la focale équivalente 35 mm au lieu du facteur de zoom",
"cameraFeaturesShowFocalLengthHint": "La focale sera déterminée après la première photo",
"film": "Pellicule",
"filmPush": "Pellicule (push)",
"filmPull": "Pellicule (pull)",

View file

@ -46,7 +46,7 @@
"cameraFeatureHistogram": "Гистограмма",
"cameraFeatureHistogramHint": "Использование гистограммы может увеличить расход аккумулятора",
"cameraFeaturesShowFocalLength": "Показать фокусное расстояние",
"cameraFeaturesShowFocalLengthHint": "Показывает эквивалент фокусного расстояния (35 мм) вместо коэффициента зума",
"cameraFeaturesShowFocalLengthHint": "Фокусное расстояние будет определено после первого снимка",
"film": "Пленка",
"filmPush": "Пленка (push)",
"filmPull": "Пленка (pull)",

View file

@ -46,7 +46,7 @@
"cameraFeatureHistogram": "直方图",
"cameraFeatureHistogramHint": "启用直方图会增加电池消耗",
"cameraFeaturesShowFocalLength": "显示焦距",
"cameraFeaturesShowFocalLengthHint": "显示 35mm 等效焦距而非变焦倍数",
"cameraFeaturesShowFocalLengthHint": "焦距将在拍摄第一张照片后确定",
"film": "胶片",
"filmPush": "胶片 (push)",
"filmPull": "胶片 (pull)",

View file

@ -250,6 +250,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
final bytes = await file.readAsBytes();
Directory(file.path).deleteSync(recursive: true);
final tags = await readExifFromBytes(bytes);
_meteringInteractor.setCameraFocalLength(focalLengthFromTags(tags));
return evFromTags(tags);
} catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace);

View file

@ -73,6 +73,7 @@ class MockCameraContainerBloc extends CameraContainerBloc {
try {
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
final tags = await readExifFromBytes(bytes);
_meteringInteractor.setCameraFocalLength(focalLengthFromTags(tags));
return evFromTags(tags);
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);

View file

@ -6,6 +6,7 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
const String _isoExifKey = 'EXIF ISOSpeedRatings';
const String _apertureExifKey = 'EXIF FNumber';
const String _shutterSpeedExifKey = 'EXIF ExposureTime';
const String _focalLengthIn35mmExifKey = "EXIF FocalLengthIn35mmFilm";
double evFromTags(Map<String, IfdTag> tags) {
final iso = double.tryParse("${tags[_isoExifKey]}");
@ -28,3 +29,14 @@ double evFromTags(Map<String, IfdTag> tags) {
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
}
int focalLengthFromTags(Map<String, IfdTag> tags) {
final focalLengthIn35mm = int.tryParse("${tags[_focalLengthIn35mmExifKey]}");
if (focalLengthIn35mm == null) {
throw ArgumentError(
'Error parsing focal length',
['$_focalLengthIn35mmExifKey: ${tags[_focalLengthIn35mmExifKey]}'].join(', '),
);
}
return focalLengthIn35mm;
}

View file

@ -7,6 +7,7 @@ void main() {
late CaffeineService service;
const methodChannel = MethodChannel('com.vodemn.lightmeter/keepScreenOn');
Future<Object?>? methodCallSuccessHandler(MethodCall methodCall) async {
switch (methodCall.method) {
case "isKeepScreenOn":
@ -20,57 +21,45 @@ void main() {
setUp(() {
service = const CaffeineService();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
CaffeineService.caffeineMethodChannel,
methodCallSuccessHandler,
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, methodCallSuccessHandler);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
CaffeineService.caffeineMethodChannel,
null,
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
});
group(
'isKeepScreenOn()',
() {
test('true', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
CaffeineService.caffeineMethodChannel,
null,
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
CaffeineService.caffeineMethodChannel,
(methodCall) async {
switch (methodCall.method) {
case "isKeepScreenOn":
return true;
default:
return null;
}
},
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, (methodCall) async {
switch (methodCall.method) {
case "isKeepScreenOn":
return true;
default:
return null;
}
});
expectLater(service.isKeepScreenOn(), completion(true));
});
test('false', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
CaffeineService.caffeineMethodChannel,
null,
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
CaffeineService.caffeineMethodChannel,
(methodCall) async {
switch (methodCall.method) {
case "isKeepScreenOn":
return false;
default:
return null;
}
},
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, (methodCall) async {
switch (methodCall.method) {
case "isKeepScreenOn":
return false;
default:
return null;
}
});
expectLater(service.isKeepScreenOn(), completion(false));
});
},

View file

@ -11,13 +11,11 @@ void main() {
{
'spotMetering': true,
'histogram': true,
'showFocalLength': true,
},
),
{
CameraFeature.spotMetering: true,
CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
},
);
});
@ -28,7 +26,6 @@ void main() {
{
CameraFeature.spotMetering: false,
CameraFeature.histogram: false,
CameraFeature.showFocalLength: false,
},
);
});

View file

@ -272,7 +272,6 @@ void main() {
{
CameraFeature.spotMetering: false,
CameraFeature.histogram: false,
CameraFeature.showFocalLength: false,
},
);
});
@ -285,7 +284,6 @@ void main() {
{
CameraFeature.spotMetering: false,
CameraFeature.histogram: true,
CameraFeature.showFocalLength: false,
},
);
});
@ -294,18 +292,17 @@ void main() {
when(
() => sharedPreferences.setString(
UserPreferencesService.cameraFeaturesKey,
"""{"spotMetering":false,"histogram":true,"showFocalLength":true}""",
"""{"spotMetering":false,"histogram":true}""",
),
).thenAnswer((_) => Future.value(true));
service.cameraFeatures = {
CameraFeature.spotMetering: false,
CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
};
verify(
() => sharedPreferences.setString(
UserPreferencesService.cameraFeaturesKey,
"""{"spotMetering":false,"histogram":true,"showFocalLength":true}""",
"""{"spotMetering":false,"histogram":true}""",
),
).called(1);
});

View file

@ -27,14 +27,14 @@ void main() {
localPlatform = _MockLocalPlatform();
service = VolumeEventsService(localPlatform);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
VolumeEventsService.volumeMethodChannel,
VolumeEventsService.volumeHandlingChannel,
methodCallSuccessHandler,
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
VolumeEventsService.volumeMethodChannel,
VolumeEventsService.volumeHandlingChannel,
null,
);
});

View file

@ -37,7 +37,6 @@ void main() {
when(() => mockUserPreferencesService.cameraFeatures).thenReturn({
CameraFeature.spotMetering: true,
CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
});
when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
@ -228,6 +227,13 @@ void main() {
expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: false"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: false"), findsNWidgets(2));
verify(
() => mockUserPreferencesService.meteringScreenLayout = {
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: false,
MeteringScreenLayoutFeature.equipmentProfiles: true,
},
).called(1);
},
);
@ -254,7 +260,6 @@ void main() {
onPressed: () => UserPreferencesProvider.of(context).setCameraFeature({
CameraFeature.spotMetering: true,
CameraFeature.histogram: false,
CameraFeature.showFocalLength: false,
}),
child: const Text(''),
),
@ -265,18 +270,15 @@ void main() {
// Match `findsNWidgets(2)` to verify that `cameraFeatureOf` specific results are the same as the whole config
expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2));
expect(find.text("${CameraFeature.histogram}: true"), findsNWidgets(2));
expect(find.text("${CameraFeature.showFocalLength}: true"), findsNWidgets(2));
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2));
expect(find.text("${CameraFeature.histogram}: false"), findsNWidgets(2));
expect(find.text("${CameraFeature.showFocalLength}: false"), findsNWidgets(2));
verify(
() => mockUserPreferencesService.cameraFeatures = {
CameraFeature.spotMetering: true,
CameraFeature.histogram: false,
CameraFeature.showFocalLength: false,
},
).called(1);
},

View file

@ -11,7 +11,7 @@ void main() {
() async {
final bytes = File('assets/camera_stub_image.jpg').readAsBytesSync();
final tags = await readExifFromBytes(bytes);
expect(evFromTags(tags), 8.25230310752341);
expectLater(evFromTags(tags), completion(8.25230310752341));
},
);
@ -20,7 +20,7 @@ void main() {
() async {
final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync();
final tags = await readExifFromBytes(bytes);
expect(() => evFromTags(tags), throwsArgumentError);
expectLater(evFromTags(tags), throwsArgumentError);
},
);
});