diff --git a/.gitignore b/.gitignore index 124f2c1d2386d8e0211ade232b860611b597d4cb..46ed4410cb88a4b3c28cce54185fa88737340f95 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,10 @@ _other/ _saved/ .gradle/ -.idea/ + +.idea/* +!.idea/inspectionProfiles + build/ app/release/ gfx/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000000000000000000000000000000000..4c67987dd01db57e7cbed94096dbb130df782ea0 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,40 @@ + + + + \ No newline at end of file diff --git a/_docs/.htaccess b/_docs/.htaccess new file mode 100644 index 0000000000000000000000000000000000000000..1f8de971d560d561bab3207b8f939bd5ae06b291 --- /dev/null +++ b/_docs/.htaccess @@ -0,0 +1,3 @@ +RewriteEngine On +RewriteCond %{HTTP:X-Forwarded-Proto} !https +RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] diff --git a/_docs/baseline_add_a_photo_white_48.png b/_docs/baseline_add_a_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..17a587b41c4d79237ddb7572b51191359e35af4f Binary files /dev/null and b/_docs/baseline_add_a_photo_white_48.png differ diff --git a/_docs/credits.html b/_docs/credits.html index e015ac0b57441e2c0e213b8d525741541832e738..82f8745e83f97f85340c1e1de188fb0c59ea1d34 100644 --- a/_docs/credits.html +++ b/_docs/credits.html @@ -16,7 +16,7 @@ "message":"This website uses cookies, including for Google Analytics and to display ads", "dismiss":"Got it!", "learnMore":"More info", - "link":"http://opencamera.org.uk/privacy_oc.html", + "link":"privacy_oc.html", "theme":"dark-bottom"}; @@ -51,15 +51,17 @@

Both rear physical cameras are available to Open Camera; also the two modes for the front camera ("cropped" and "wide") are available to Open Camera.

@@ -221,7 +226,6 @@ manual controls, RAW and 120fps video.

Open Camera Privacy Policy.

This website uses icons from third party sources, see licences.

Open Camera on Sourceforge.

-

More of my Free software.


diff --git a/_docs/help.html b/_docs/help.html index 13804dbadb383615640423e433538412f7fca1e4..0ebedc42bfd0a85fede2cb6f334e482532fc31a2 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -23,7 +23,7 @@ "message":"This website uses cookies, including for Google Analytics and to display ads", "dismiss":"Got it!", "learnMore":"More info", - "link":"http://opencamera.org.uk/privacy_oc.html", + "link":"privacy_oc.html", "theme":"dark-bottom"}; @@ -56,11 +56,6 @@ -

More of my Free software: -Vibrance HDR ~ - -

- Open Camera icon

Open Camera Help

@@ -120,17 +115,27 @@ you want to switch off the display, do so on your device manually).

Take Photo iconTake photo - Click to take a photo. In some cases, you can also hold (long press) for a continuous burst:

Switch to video iconSwitch to video mode - Clicking the - smaller video icon will switch to video mode. The photo and video icons will then swap: click the larger video icon to - start/stop video recording, and click the smaller photo icon to switch back to photo mode.

+ smaller video icon will switch to video mode. The photo and video icons will then swap: click the larger video icon to + start/stop video recording, and click the smaller photo icon to switch back to photo mode.

Switch Camera iconSwitch camera - Switches between front and back camera (if your -device has two cameras).

+device has both front and back cameras). If your device has more than one front and/or back camera, then this will switch +between the first front and back camera.

+ +

Switch multi-camera iconSwitch multi-camera icon - + This icon only shows on devices with more than one front and/or back cameras, and allows you to switch between those + cameras. For example, a device might have two back cameras, one standard and one ultra-wide, this icon will switch between + the standard and ultra-wide camera. + If Settings/On screen GUI/"Multiple cameras icon" is disabled, then this icon will not show; instead the "Switch camera" + icon can by used to cycle through all the cameras. + Note that some devices do not allow third party applications to access their multiple cameras, in which case Open Camera isn't + able to use them.

Lock Exposure iconExposure lock - Click to lock or unlock the exposure. When locked, the icon will change to Exposure Locked icon. Locking the exposure means @@ -139,100 +144,101 @@ compensation). Note that this isn't guaranteed to work on all devices (doesn't s

Exposure Compensation iconExposure compensation, ISO and Manual White Balance - Clicking this will bring a panel with various controls:

To get rid of this panel, either click the Exposure button again, or click elsewhere on the screen.

-

Popup iconPopop menu - Opens the popup menu for quick access to changing +

Popup iconPopup menu - Opens the popup menu for quick access to changing various options:

+

Can you implement disabling shutter sound for my phone? - + If Open Camera shows the option Settings/"Camera API", then changing to "Camera2 API" means you'll be able to disable + shutter sounds under "Settings/More camera controls...". + When not using Camera2 API, if the option "Shutter sound" under "More camera controls..." isn't shown, + then it's not available. There + are possible workarounds for some of these devices (which is why some third party camera applications + may be able to silence the shutter), though the issue is these don't work on all devices, and tend + to use methods that Google now discourage. The fault is with the device for not supporting + standard method for cameras to disable the shutter sound on Android. In particular, if under Settings/About + you see that "Can disable shutter sound?" says No, it means the device's camera API is telling 3rd + party camera apps that shutter sound can't be disabled (so either it can't do it, or the API is lying + - either way, this should be reported to your manufacturer).

+

Photos or videos fail to save! - Firstly, if you're trying to save to an external SD card, see "How can I save to my external SD card?" above. Otherwise:

+

I switched to a new phone, and now something doesn't work! - Google's auto-backup will +typically transfer settings to a new phone, but this may mean a camera-specific setting is no +longer relevant. In particular, if you set a non-default save location, it may be that the path is +not valid on the new device, or if using Settings/More camera controls/"Use Storage Access Framework", +you may need to rechoose the save location (from Settings/More camera controls/"Save location") to +grant permission to the new device. You can use Settings/Settings manager/"Reset settings" to reset +Open Camera to its original state, to rule out any issues from an Android backup from another +device.

+

My pictures are being rotated/cropped! - This likely means the auto-level option is on. (If they're being rotated even when the phone is held level, it may mean the accelerometer sensor on your device isn't calibrated.) It's off by default, but you may have accidentally switched it on. To turn off, go to the "popup" menu @@ -1189,9 +1269,11 @@ and untick Auto-level.

Why doesn't Open Camera support dual / multiple cameras? - Open Camera supports switching between all cameras that are made available to third party applications. Usually this means front and back cameras, but some devices have -dual back-facing cameras - in some cases the extra camera isn't made available to third party applications, so it isn't -possible for Open Camera to support them. Even where they are, since there is no current standard on what the extra -cameras are used for, it can't do anything with them other than allowing you to switch between them.

+multiple front and/or back-facing cameras. Use the +Switch multi-camera iconswitch multi-camera icon +to switch between multiple front or back cameras. In some cases the extra cameras aren't made available to third party +applications, so it isn't possible for Open Camera to support them. Even where they are, since there is no current standard +on what the extra cameras are used for, it can't do anything with them other than allowing you to switch between them.

Why doesn't Open Camera support the maximum video resolution on my device? - If you are using Camera2 API, make sure that you're not in slow motion mode (see "Speed" under @@ -1209,8 +1291,8 @@ camera, and there is no guarantee that they will be met. For best chance of succ the standard Android camera API. Some devices do now support high speed frame rates when Camera2 API is enabled.

Why doesn't Open Camera show 23MP resolution on my Sony Xperia, only 8MP? - This was a problem on older devices - and/or with the old Camera API because of Sony not making this available to third party camera applications. On newer Sony - devices, this should become available if you set Settings/"Camera API" to "Camera2 API".

+ and/or with the old Camera API because of Sony not making this available to third party camera applications. On newer Sony + devices, this should become available if you set Settings/"Camera API" to "Camera2 API".

Why does the resolution of my photos not match the specified camera resolution? - This happens if auto-level is enabled. The image is rotated to be level, which means the @@ -1222,11 +1304,11 @@ standard Android API for 3rd party camera apps to use.

Why doesn't touch to focus work? - Touching the screen should allow you to choose a particular region to focus on. If this doesn't work:

I get "FAILED TO OPEN CAMERA" - In some cases this is fixed by restarting @@ -1290,20 +1372,11 @@ default and you want to change it, then go to the App Settings for that app, and actions, with an option to clear them. There are plenty of gallery apps for Android, and it seems better for users to have this choice, rather than Open Camera having its own custom gallery.

-

Can you implement disabling shutter sound for my phone? - -If Open Camera shows the option Settings/"Camera API", then changing to "Camera2 API" means you'll be able to disable -shutter sounds under "Settings/More camera controls...". -When not using Camera2 API, if the option "Shutter sound" under "More camera controls..." isn't shown, -then it's not available. There -are possible workarounds for some of these devices (which is why some third party camera applications -may be able to silence the shutter), though the issue is these don't work on all devices, and tend -to use methods that Google now discourage. The fault is with the device for not supporting -standard method for cameras to disable the shutter sound on Android. In particular, if under Settings/About -you see that "Can disable shutter sound?" says No, it means the device's camera API is telling 3rd -party camera apps that shutter sound can't be disabled (so either it can't do it, or the API is lying -- either way, this should be reported to your manufacturer).

- -

Why does Open Camera have ads? - Open Camera does not have ads (there may be ads on the online +

Clicking on the thumbnail icon only shows the photo briefly? - This can happen if you've changed the save location +for photos/videos to one that is not typical (e.g., not inside DCIM/ ). Some gallery applications will not show a photo in +such cases.

+ +

Why does Open Camera have ads? - Open Camera does not have ads in the application (there may be ads on the online webpage you're reading now, but not in the app). There are however some clones on Google Play with ads inserted. Please ensure that you've downloaded from one of the places listed above on this page.

@@ -1324,6 +1397,11 @@ supported in both old and Camera2 API. See Note that some Google Pixels (e.g., 3a) do not have a Pixel Visual Core chip, and may not support HDR+ in third party camera applications (see this thread).

+

Why isn't Panorama supported on my device? - To support panorama in Open Camera, this requires Android 5+, +a gyroscope and compass, and at least 256MB of "large heap" memory (note, this isn't the same as the device's RAM). +Bear in mind that even if your device supports panorama, with Open Camera I have to support thousands of Android devices, +and I don't have the luxury of targetting functionality towards one particular device.

+

Why doesn't Open Camera's HDR images look like other HDR camera apps? - There are a great many different ways of applying a HDR algorithm, some may do better or worse in different circumstances; some may look more or less pleasing depending on what you are after. Also note that some camera apps use "HDR" to mean "apply a whacky-looking filter". @@ -1377,15 +1455,8 @@ no longer seems to work properly, try a reboot of your device, or if that fails settings to the defaults (under Settings/Settings manager/"Reset settings").

If there's still a problem, please check other third party camera applications to see if they have the same - problem or not. (It's not enough to try your device's "stock" camera - in some cases, devices may have bugs for - third party camera applications that don't affect the stock camera.) If the issue is specific to Camera2 API, then - please check third party camera applications also using Camera2, e.g.:

- + problem or not. (It's not enough to try your device's "stock" camera - in some cases, devices may have bugs for + third party camera applications that don't affect the stock camera.)

If you find a bug, please report it here (please check for existing tickets first). @@ -1394,21 +1465,19 @@ can paste the information into your web browser, email or whatever.

For more general questions or things like feature suggestions, please use the forums. -For some enquiries (e.g., requests for specific projects you are working on), you may prefer to use email. +For some enquiries you may prefer to use email. Please contact me at mark.harman.apps@gmail.com. Please note that I get a lot of emails for Open Camera these days - I try to reply as many as I can, but this is not always feasible. I do however read every email and forum post.

Note that whilst I welcome reviews/ratings, they are not a good way for reporting bugs (I may -miss it, there's only limited number of characters for me to reply, and I don't get notified of -further replies).

+miss it, there's only limited number of characters for me to reply).


Open Camera Privacy Policy.

This website uses icons from third party sources, see licences.

Open Camera on Sourceforge.

-

More of my Free software.


diff --git a/_docs/history.html b/_docs/history.html index 4f73769f287e2b42f7753e23eda07cbbf7532906..2ff449e9d6f6903943b03f7855390cc4627bed0f 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -16,7 +16,7 @@ "message":"This website uses cookies, including for Google Analytics and to display ads", "dismiss":"Got it!", "learnMore":"More info", - "link":"http://opencamera.org.uk/privacy_oc.html", + "link":"privacy_oc.html", "theme":"dark-bottom"}; @@ -48,6 +48,93 @@

< Main Page.

+Version 1.48.1 (2020/05/02)
+
+FIXED   Crash on devices with Camera2 API where camera reports no picture, video or preview
+        resolutions, instead fail to open camera gracefully instead.
+FIXED   Fix switch camera buttons behaviour if a camera with ID greater than 0 failed to open.
+FIXED   Some devices lost custom video profiles in 1.48.
+ADDED   If camera fails to open, display ID of current camera that we tried to open.
+
+Version 1.48 (2020/04/22)
+
+FIXED   Taking front camera photos with frontscreen torch was slow.
+FIXED   When using "Pause after taking photo", touching to unpause no longer
+        triggers auto focus, or taking another photo for "Touch to capture".
+FIXED   Take photo widget issue.
+FIXED   Camera specific hardware keys such as volume keys shouldn't take effect in settings etc.
+FIXED   Don't set optical image stabilization if video digital stabilization is enabled in video
+        mode.
+FIXED   Seamless video restart on maximum filesize (for Android 8+) wasn't broadcasting video files
+        except the last one, meaning they were taking longer to show up in mediastore gallery.
+FIXED   Recording video on Android 8+ could leave zero-size files if size approached the maximum
+        filesize, but a restart did not occur.
+FIXED   Problem of on-screen level angle overlapping with shutter icon when using a widescreen
+        preview aspect ratio.
+FIXED   Incorrect layout for on-screen text when using "icons along top" with wide-screen aspect
+        ratio and device held in upside-down landscape orientation.
+FIXED   Focus seekbars overlapped with histogram in widescreen aspect ratio when using "Icons along
+        top" UI placement.
+FIXED   Was incorrectly offering manual white balance even if camera didn't support this
+        (inconsistency that the manual white balance option showed, even though the manual white
+        balance temperature seekbar wasn't shown).
+FIXED   Optional on-screen icons (such as flash, RAW) weren't updating correctly if switching to a
+        camera that didn't support that feature.
+FIXED   Don't show on-screen flash icon in video mode (since this icon doesn't support torch, and
+        flash auto/on not supported in video mode).
+FIXED   Preview texture buffer size (for Camera2 API) could be set incorrectly after changing aspect
+        ratios.
+FIXED   Auto-level photos could never be full resolution (for when angle was 0).
+FIXED   Update on-screen time format more often when changing device settings.
+FIXED   USB/bluetooth keyboard control bug when navigating popup menu, if icons were displayed with a
+        horizontal scrollbar.
+ADDED   New icon for switching between multiple cameras. If your device has multiple front and/or
+        back cameras, then the existing icon to switch cameras will switch between the first front
+        and back camera; the new icon will instead cycle between the multiple front or back cameras.
+        If you prefer the old behaviour, then disable
+        Settings/On screen GUI/"Multiple cameras icon".
+ADDED   Current camera ID now displayed on-screen (next to date/time) for devices with multiple
+        front/back cameras. This can be disabled under Settings/Camera preview/"Show camera ID".
+ADDED   Aperture control, for devices that support this. (Camera2 API only.)
+ADDED   Flash on and torch now supported for manual ISO/exposure.
+ADDED   Option to specify REC709 or sRGB profile for video recording.
+ADDED   New custom gamma profile option for video recording.
+ADDED   New video profiles JTVideo, JTLog and JTLog2 (thanks to JT Haapala).
+ADDED   New option for alpha value to use for ghost image option.
+ADDED   More zebra stripe values 93-99%.
+ADDED   Options to control zebra stripe colours.
+ADDED   Option for storing device's current yaw/pitch/roll in Exif user comment for photos (thanks
+        to Joshua).
+ADDED   New option Settings/More camera controls/"Allow long press actions" to disable long press
+        actions.
+UPDATED Auto-level feature now shows on-screen rectangle to show the frame of the resultant photo.
+UPDATED Improvements for log profiles for video recording. Please note that this means the behaviour
+        of these profiles has changed!
+UPDATED On devices with on-screen navigation buttons, camera preview can now display under these
+        buttons if required for wide aspect ratio (requires Android 5+).
+UPDATED New immersive mode option to hide navigation buttons only when in immersive mode; existing
+        option for hiding navigation buttons now renamed to say "dim".
+UPDATED Show toast with camera id on startup if camera isn't set to the default camera for front or
+        back facing. Toast for cameras also displays whether ultra-wide, when using Camera2 API.
+UPDATED HDR and NR photo modes now limited to maximum resolution of 22 megapixels (to avoid risk of
+        running out of memory on devices with large camera resolutions).
+UPDATED Improved performance when displaying ghost image larger than device's resolution.
+UPDATED Popup menu now displays extra information for resolutions (MP for photos, descriptive name
+        like FullHD, VGA etc for video).
+UPDATED Don't set video digital stabilization when in photo mode.
+UPDATED Some preferences are now showed disabled if only relevant for another nearby option that
+        isn't currently enabled.
+UPDATED Moved video bitrate and frame rate options to debugging section.
+UPDATED Improved UI support for "external" cameras (if detected/supported with Camera2 API).
+UPDATED Improved placement of on-screen text (zoom, video recording time etc) to avoid focus
+        seekbars in landscape mode.
+UPDATED Improved look of on-screen text for manual/exposure sliders.
+UPDATED Exposure icon now highlights red when exposure UI is open.
+UPDATED Exposure UI now auto-opens when switching to manual white balance (as exposure UI contains
+        the manual white balance temperature seekbar).
+UPDATED More repeat mode options (100x, 200x, 500x).
+UPDATED Optimisation for reading most recent photo/video for thumbnail.
+
 Version 1.47.3 (2019/10/20)
 
 FIXED   Grids were being drawing too faintly.
@@ -1523,7 +1610,6 @@ First release.
 

Open Camera Privacy Policy.

This website uses icons from third party sources, see licences.

Open Camera on Sourceforge.

-

More of my Free software.


diff --git a/_docs/index.html b/_docs/index.html index bbab5e5d965446cfe36d2353bb23f4e4a4a3301c..6f5829bd1d450466e8d7478ceaa20a45eee76fe3 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -23,7 +23,7 @@ "message":"This website uses cookies, including for Google Analytics and to display ads", "dismiss":"Got it!", "learnMore":"More info", - "link":"http://opencamera.org.uk/privacy_oc.html", + "link":"privacy_oc.html", "theme":"dark-bottom"}; @@ -126,10 +126,11 @@ browsers -->

Download on Google Play.

-
-

Open Camera is completely free, however if you wish you can show your appreciation and support future development by purchasing -my donation app on Google Play. +

Also see alternative download sites.

+
+

Open Camera is completely free, however if you wish you can show your appreciation by supporting me.

+

Open Camera Blog ~ @@ -141,9 +142,10 @@ browsers -->

@@ -163,7 +165,7 @@ browsers -->
-

Requirements

+

Requirements

Open Camera requires Android 4.0.3 or better. Some features may only be available on some devices (it may depend on Android version, or require specific support from the camera/device).

@@ -176,19 +178,20 @@ your wedding etc :)

Instructions

-

Credits

+

Credits

Open Camera is written by Mark Harman with additional contributors, see credits for details.

-

Privacy policy

+

Privacy policy

See my privacy policy for details.

-

Licence

+

Licence and Terms of Service

+

Open Camera is released under the GPL v3 or later. The source code is available from @@ -209,6 +212,7 @@ Also see "Can I use the Open Camera source code in my app?" under the https://google.github.io/material-design-icons/, by Google, under Apache license version 2.0 (some cases include modifications, no need to credit me). In particular: + baseline_add_a_photo_white_48.png, baseline_bluetooth_white_48.png, baseline_check_white_48.png, baseline_close_white_48.png, baseline_filter_vintage_white_48.png, baseline_folder_open_white_48.png, baseline_highlight_white_48.png, @@ -224,7 +228,7 @@ Also see "Can I use the Open Camera source code in my app?" under the "Can I use the Open Camera source code in my app?" under the Modified versions of some of these icons are also used on this website. -
Open Camera's app icon/logo also makes use of ic_photo_camera_white_48dp by Google (also Apache license version 2.0). +
Open Camera's app icon/logo also makes use of ic_photo_camera by Google (also Apache license version 2.0).

Note that old versions of Open Camera also used the following:

@@ -291,7 +295,6 @@ of old versions; but no need to mention CC0 media which doesn't require attribut

Open Camera Privacy Policy.

This website uses icons from third party sources, see licences.

Open Camera on Sourceforge.

-

More of my Free software.


diff --git a/_docs/info.html b/_docs/info.html new file mode 100644 index 0000000000000000000000000000000000000000..11f210a69d7f48360757d0ee8e0d611787e7eaee --- /dev/null +++ b/_docs/info.html @@ -0,0 +1,121 @@ + + + +Open Camera + + + + + + + + + + + + + + + + + + + + + + +Open Camera icon +
+

Open Camera

+
+ +

< Main Page.

+ + + + + + + +

Support me!

+ + + +

Open Camera is completely free, however if you wish you can show your appreciation to me by making an optional payment. + Please note that this is not required, and that doing so will not provide any additional features (or remove the ads I have on the + website). However for those who wish to make a contribution to me, this is greatly appreciated!

+ +

You can contribute through PayPal, simply click the "Buy Now" button, and enter the amount you wish to give me:

+ +
+ + + + +
+ +

...or you can also set up a regular contribution:

+ + +
+ + + + +
+ + + +
+ +

(PayPal account not required, supports debit or credit card). +Thanks!

+ +

Please note that these contributions do not constitute a charitable donation, and hence not eligible for tax-deduction. (If you have found my webpage because you are using a camera app with ads inside it, then you may have downloaded a third party clone that is not distributed by me. For my Open Camera app, please make sure you've downloaded from the links given on my Main Page.)

+ +

Privacy policy: Please note that some information such as name, email, amount will be shared to me by PayPal. All PayPal transactions are subject to the PayPal Privacy Policy (alternatively known as PayPal's Privacy Statement).

+ +

Alternative download sites

+ + + +
+

Open Camera Privacy Policy.

+

This website uses icons from third party sources, see licences.

+

Open Camera on Sourceforge.

+
+ + + diff --git a/_docs/privacy_oc.html b/_docs/privacy_oc.html index 46aa6f8f0fccda6dd680a73797bf2a71dbc7d40b..38972821d7f243aa56c221b1deab5488cb2991dd 100644 --- a/_docs/privacy_oc.html +++ b/_docs/privacy_oc.html @@ -16,7 +16,7 @@ "message":"This website uses cookies, including for Google Analytics and to display ads", "dismiss":"Got it!", "learnMore":"More info", - "link":"http://opencamera.org.uk/privacy_oc.html", + "link":"privacy_oc.html", "theme":"dark-bottom"}; @@ -55,11 +55,11 @@ of taking photos and recording videos, to fulfil its purpose as a camera. Microphone permission is also used for the optional "Audio control" options.

Open Camera requires permission to "access photos, media and files on your devices", as this permission is required for Android to -save resultant photos and videos to your device.

+save resultant files such as photos and videos to your device.

-

Location permission is required for geotagging options (for photos and videos, including stamp and subtitles options). - Geotagging is disabled by default, but if enabled, your location is encoded into saved image and video files. - Location permission is also required to connect to Bluetooth LE remote control devices.

+

Location permission is required for the optional geotagging features (for photos and videos, including stamp and subtitles options). + When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files. + Location permission is also required to connect to Bluetooth remote control devices.

Bluetooth permission is required for communicating with some supported Bluetooth remote control devices.

@@ -80,7 +80,7 @@ save resultant photos and videos to your device.

-

Although Open Camera is ad-free, this website has ads via Google Adsense: Third party vendors, including Google, use cookies to +

Although Open Camera is ad-free, the Open Camera website has ads via Google Adsense: Third party vendors, including Google, use cookies to serve ads based on a user's previous visits to this website or other websites. Google's use of advertising cookies enables it and its partners to serve ads based on people's visit to this sites and/or other sites on the Internet. You may opt out of personalised advertising by visiting Google's Ads Settings. Alternatively, you can @@ -89,7 +89,7 @@ opt out of some third-party vendors' uses of cookies for personalised advertisin

Update: I have instructed Google to not display personalised ads to users in the EEA.

-

This website also uses Google Analytics which uses cookies, please see their +

The Open Camera website also uses Google Analytics which uses cookies, please see their Privacy Policy for more details.

Also see "How Google uses information from sites or apps @@ -101,7 +101,6 @@ that use our services".

Open Camera Privacy Policy.

This website uses icons from third party sources, see licences.

Open Camera on Sourceforge.

-

More of my Free software.


diff --git a/app/build.gradle b/app/build.gradle index 8ee382b9b00c77ba0dc5fe67621829bfd8cfe7b1..de48a52858ab25c3a742eeccf6d97fd47d899670 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,7 @@ android { } lintOptions { + abortOnError false checkReleaseBuilds false abortOnError false } diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 4d6cfdd13eac59cdb839d16e547cd95924047690..d2fc36ed255d753005bb69c11f31f5a81f817b0d 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -11,9 +11,12 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; +import net.sourceforge.opencamera.LocationSupplier; import net.sourceforge.opencamera.MyPreferenceFragment; import net.sourceforge.opencamera.PanoramaProcessorException; import net.sourceforge.opencamera.cameracontroller.CameraController2; @@ -63,6 +66,7 @@ import android.util.Log; import android.view.Display; import android.view.KeyEvent; import android.view.View; +import android.view.WindowManager; import android.widget.SeekBar; import android.widget.TextView; import android.widget.ZoomControls; @@ -75,6 +79,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) { switchToCamera(0); @@ -483,10 +492,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 sizes = mPreview.getSupportedPreviewSizes(); CameraController.Size best_size = mPreview.getOptimalPreviewSize(sizes); Log.d(TAG, "best size: " + best_size.width + ", " + best_size.height); - assertTrue( best_size.width == mPreview.getCameraController().getPreviewSize().width ); - assertTrue( best_size.height == mPreview.getCameraController().getPreviewSize().height ); + assertEquals(best_size.width, mPreview.getCameraController().getPreviewSize().width); + assertEquals(best_size.height, mPreview.getCameraController().getPreviewSize().height); } private void checkOptimalVideoPictureSize(double targetRatio) { @@ -859,8 +868,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 sizes = mPreview.getSupportedPictureSizes(false); CameraController.Size best_size = mPreview.getOptimalVideoPictureSize(sizes, targetRatio); Log.d(TAG, "best size: " + best_size.width + ", " + best_size.height); - assertTrue( best_size.width == mPreview.getCameraController().getPictureSize().width ); - assertTrue( best_size.height == mPreview.getCameraController().getPictureSize().height ); + assertEquals(best_size.width, mPreview.getCameraController().getPictureSize().width); + assertEquals(best_size.height, mPreview.getCameraController().getPictureSize().height); } private void checkSquareAspectRatio() { @@ -967,15 +976,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) { Log.d(TAG, "switch camera"); @@ -1012,11 +1021,219 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 all_picture_sizes = new ArrayList<>(mPreview.getSupportedPictureSizes(false)); + final List std_picture_sizes = new ArrayList<>(mPreview.getSupportedPictureSizes(true)); + assertEquals(all_picture_sizes, std_picture_sizes); + assertTrue(all_picture_sizes.contains(std_size)); + + // switch to the photo mode and check we reduce the resolution + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.PhotoModePreferenceKey, photo_mode_preference); + editor.apply(); + updateForSettings(); + assertSame(mActivity.getApplicationInterface().getPhotoMode(), photo_mode); + + CameraController.Size new_size = mPreview.getCurrentPictureSize(); + Log.d(TAG, "new_size: " + new_size.width + " x " + new_size.height); + if( expect_reduce_resolution ) { + assertFalse(new_size.equals(std_size)); + assertTrue(new_size.width*new_size.height <= max_mp); + } + else { + assertEquals(new_size, std_size); + } + if( expect_supports_burst ) { + assertTrue(new_size.supports_burst); + } + final List all_picture_sizes_new = new ArrayList<>(mPreview.getSupportedPictureSizes(false)); + final List picture_sizes_new = new ArrayList<>(mPreview.getSupportedPictureSizes(true)); + assertEquals(all_picture_sizes, all_picture_sizes_new); + if( expect_reduce_resolution ) { + assertTrue(picture_sizes_new.size() < all_picture_sizes.size()); + // check the filtered modes are a subset of all of them + assertTrue(all_picture_sizes.containsAll(picture_sizes_new)); + // check all of the filtered modes satisfy the max_mp + for(CameraController.Size size : picture_sizes_new) { + assertTrue(size.width*size.height <= max_mp); + } + } + else { + assertEquals(all_picture_sizes, picture_sizes_new); + } + if( expect_supports_burst ) { + // check all of the filtered modes support burst + for(CameraController.Size size : picture_sizes_new) { + assertTrue(size.supports_burst); + } + } + // check the filtered modes include the chosen mode + assertTrue(picture_sizes_new.contains(new_size)); + + // pause and resume, check resolutions unchanged + pauseAndResume(); + assertSame(mActivity.getApplicationInterface().getPhotoMode(), photo_mode); + CameraController.Size new_size2 = mPreview.getCurrentPictureSize(); + assertEquals(new_size, new_size2); + final List all_picture_sizes_new2 = new ArrayList<>(mPreview.getSupportedPictureSizes(false)); + final List picture_sizes_new2 = new ArrayList<>(mPreview.getSupportedPictureSizes(true)); + assertEquals(all_picture_sizes_new, all_picture_sizes_new2); + assertEquals(picture_sizes_new, picture_sizes_new2); + + CameraController.Size change_to_size = null; + String settings_size = ""; + if( test_change_resolution ) { + // test changing the resolution in the new mode + + // save old resolution + settings_size = settings.getString(PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId()), ""); + + // find a different resolution + for(CameraController.Size size : picture_sizes_new) { + if( !size.equals(new_size) ) { + change_to_size = size; + break; + } + } + assertNotNull(change_to_size); + Log.d(TAG, "set size to " + change_to_size.width + " x " + change_to_size.height); + settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + editor = settings.edit(); + editor.putString(PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId()), change_to_size.width + " " + change_to_size.height); + editor.apply(); + updateForSettings(); + + CameraController.Size new_size3 = mPreview.getCurrentPictureSize(); + assertEquals(change_to_size, new_size3); + assertFalse(new_size.equals(new_size3)); + } + + // switch back to STD, and check we return to the original resolution (or not, if test_change_resolution==true) + settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + editor = settings.edit(); + editor.putString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std"); + editor.apply(); + updateForSettings(); + assertSame(mActivity.getApplicationInterface().getPhotoMode(), MyApplicationInterface.PhotoMode.Standard); + + new_size = mPreview.getCurrentPictureSize(); + if( test_change_resolution ) { + assertEquals(change_to_size, new_size); + } + else { + assertEquals(std_size, new_size); + } + final List all_picture_sizes2 = new ArrayList<>(mPreview.getSupportedPictureSizes(false)); + final List std_picture_sizes2 = new ArrayList<>(mPreview.getSupportedPictureSizes(true)); + assertEquals(all_picture_sizes, all_picture_sizes2); + assertEquals(all_picture_sizes, std_picture_sizes2); + assertTrue(std_picture_sizes2.contains(new_size)); + + if( test_change_resolution ) { + // set back, so we don't confuse later parts of the test + settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + editor = settings.edit(); + editor.putString(PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId()), settings_size); + editor.apply(); + updateForSettings(); + } + } + + /* Ensures that we enforce a maximum resolution correctly in some photo modes. + */ + public void testResolutionMaxMP() { + Log.d(TAG, "testResolutionMaxMP"); + + setToDefault(); + + CameraController.Size std_size = mPreview.getCurrentPictureSize(); + assertNotNull(std_size); + Log.d(TAG, "std_size: " + std_size.width + " x " + std_size.height); + + int max_mp = (std_size.width*std_size.height-100); + mActivity.getApplicationInterface().test_max_mp = max_mp; + Log.d(TAG, "test_max_mp: " + mActivity.getApplicationInterface().test_max_mp); + + if( mActivity.supportsHDR() ) { + subTestResolutionMaxMP("preference_photo_mode_hdr", MyApplicationInterface.PhotoMode.HDR, max_mp, false, true, true); + subTestResolutionMaxMP("preference_photo_mode_hdr", MyApplicationInterface.PhotoMode.HDR, max_mp, true, true, true); + } + if( mActivity.supportsNoiseReduction() ) { + subTestResolutionMaxMP("preference_photo_mode_noise_reduction", MyApplicationInterface.PhotoMode.NoiseReduction, max_mp, false, true, true); + subTestResolutionMaxMP("preference_photo_mode_noise_reduction", MyApplicationInterface.PhotoMode.NoiseReduction, max_mp, true, true, true); + } + if( mActivity.supportsDRO() ) { + subTestResolutionMaxMP("preference_photo_mode_dro", MyApplicationInterface.PhotoMode.DRO, max_mp, false, false, false); + subTestResolutionMaxMP("preference_photo_mode_dro", MyApplicationInterface.PhotoMode.DRO, max_mp, true, false, false); + } + } + + /* Ensures that we handle correctly when the largest resolution doesn't support burst. + */ + public void testResolutionBurst() { + Log.d(TAG, "testResolutionBurst"); + + if( !mPreview.usingCamera2API() ) { + Log.d(TAG, "test requires camera2 api"); + return; + } + + setToDefault(); + + mPreview.test_burst_resolution = true; + pauseAndResume(); // needed for test_burst_resolution to take effect + + CameraController.Size std_size = mPreview.getCurrentPictureSize(); + // check the test_burst_resolution flag took effect: + assertFalse(std_size.supports_burst); + + // now find the maximum mp that supports burst + final List all_picture_sizes = new ArrayList<>(mPreview.getSupportedPictureSizes(false)); + int max_mp = 0; + for(CameraController.Size size : all_picture_sizes) { + if( size.supports_burst ) { + int mp = size.width*size.height; + max_mp = Math.max(max_mp, mp); + } + } + Log.d(TAG, "max_mp: " + max_mp); + assertTrue(max_mp < std_size.width*std_size.height); + + if( mActivity.supportsHDR() ) { + subTestResolutionMaxMP("preference_photo_mode_hdr", MyApplicationInterface.PhotoMode.HDR, max_mp, false, true, true); + subTestResolutionMaxMP("preference_photo_mode_hdr", MyApplicationInterface.PhotoMode.HDR, max_mp, true, true, true); + } + if( mActivity.supportsNoiseReduction() ) { + subTestResolutionMaxMP("preference_photo_mode_noise_reduction", MyApplicationInterface.PhotoMode.NoiseReduction, max_mp, false, true, true); + subTestResolutionMaxMP("preference_photo_mode_noise_reduction", MyApplicationInterface.PhotoMode.NoiseReduction, max_mp, true, true, true); + } + if( mActivity.supportsDRO() ) { + subTestResolutionMaxMP("preference_photo_mode_dro", MyApplicationInterface.PhotoMode.DRO, max_mp, false, false, false); + subTestResolutionMaxMP("preference_photo_mode_dro", MyApplicationInterface.PhotoMode.DRO, max_mp, true, false, false); + } + if( mActivity.supportsFastBurst() ) { + subTestResolutionMaxMP("preference_photo_mode_fast_burst", MyApplicationInterface.PhotoMode.FastBurst, max_mp, false, true, true); + subTestResolutionMaxMP("preference_photo_mode_fast_burst", MyApplicationInterface.PhotoMode.FastBurst, max_mp, true, true, true); } } @@ -1033,7 +1250,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 supported_focus_values = mPreview.getSupportedFocusValues(); - assertTrue( supported_focus_values != null ); + assertNotNull(supported_focus_values); assertTrue( supported_focus_values.size() > 1 ); for(String supported_focus_value : supported_focus_values) { Log.d(TAG, "supported_focus_value: " + supported_focus_value); @@ -1139,19 +1356,19 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) { int cameraId = mPreview.getCameraId(); @@ -1386,8 +1603,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 0); String next_string = mActivity.getResources().getString(next ? net.sourceforge.opencamera.R.string.next : net.sourceforge.opencamera.R.string.previous); @@ -1586,7 +1803,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) { @@ -1660,7 +1877,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 saved_count_cameraContinuousFocusMoving ); // switch to video @@ -1785,21 +2002,21 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 saved_count_cameraContinuousFocusMoving ); } @@ -1854,7 +2071,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2= Build.VERSION_CODES.O ) { assertTrue(mActivity.testHasNotification()); } @@ -2229,7 +2446,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) { Log.d(TAG, "switch camera"); @@ -2658,16 +2875,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 mPreview.getMaximumExposureTime() ) saved_exposure_time = mPreview.getMaximumExposureTime(); - assertTrue( mPreview.getCameraController().getExposureTime() == saved_exposure_time ); + assertEquals(mPreview.getCameraController().getExposureTime(), saved_exposure_time); } clickView(exposureButton); - assertTrue(exposureButton.getVisibility() == View.VISIBLE); - assertTrue(exposureContainer.getVisibility() == View.VISIBLE); - assertTrue(isoSeekBar.getVisibility() == View.VISIBLE); - assertTrue(exposureTimeSeekBar.getVisibility() == (mPreview.supportsExposureTime() ? View.VISIBLE : View.GONE)); + assertEquals(exposureButton.getVisibility(), View.VISIBLE); + assertEquals(exposureContainer.getVisibility(), View.VISIBLE); + assertEquals(isoSeekBar.getVisibility(), View.VISIBLE); + assertEquals(exposureTimeSeekBar.getVisibility(), (mPreview.supportsExposureTime() ? View.VISIBLE : View.GONE)); assertEquals(Math.min(old_max, mPreview.getMaximumISO()), mPreview.getCameraController().getISO()); if( mPreview.supportsExposureTime() ) - assertTrue( mPreview.getCameraController().getExposureTime() == saved_exposure_time ); + assertEquals(mPreview.getCameraController().getExposureTime(), saved_exposure_time); } } @@ -2825,11 +3042,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); if( !immersive_mode ) { - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); } - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); } } @@ -3362,6 +3635,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE))); + assertEquals(switchMultiCameraButton.getVisibility(), (immersive_mode ? View.GONE : (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE))); + assertEquals(switchVideoButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(audioControlButton.getVisibility() == ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); String focus_value = mPreview.getCameraController().getFocusValue(); String focus_value_ui = mPreview.getCurrentFocusValue(); @@ -3434,6 +3709,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 camera_ids) throws InterruptedException { + if( mActivity.showSwitchMultiCamIcon() ) { + int cameraId = mPreview.getCameraId(); + CameraController.Facing facing = mPreview.getCameraControllerManager().getFacing(cameraId); - if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { + do { + Log.d(TAG, "testing multi cam button..."); + View switchMultiCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_multi_camera); + clickView(switchMultiCameraButton); + waitUntilCameraOpened(); + + int new_cameraId = mPreview.getCameraId(); + Log.d(TAG, "multi cam button switched to " + new_cameraId); + Log.d(TAG, "camera_ids was: " + camera_ids); + assertTrue(new_cameraId != cameraId); + assertFalse(camera_ids.contains(new_cameraId)); + camera_ids.add(new_cameraId); + + CameraController.Facing new_facing = mPreview.getCameraControllerManager().getFacing(new_cameraId); + assertEquals(facing, new_facing); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + } + while( mActivity.getNextMultiCameraId() != cameraId ); + } + } + + /** Tests taking a photo with multiple cameras. + * Also tests the content descriptions for switch camera button. + * And tests that we save the current camera when pausing and resuming. + * @param cycle_all_cameras If true, expect that the Switch Camera icon cycles through all + * cameras. + * @param test_multi_cam If true, also test cycling through cameras using the switch multi + * camera icon. If true, then cycle_all_cameras must be false. Should + * only be true on multi-camera devices. + */ + private void subTestTakePhotoMultiCameras(boolean cycle_all_cameras, boolean test_multi_cam) throws InterruptedException { + Log.d(TAG, "subTestTakePhotoMultiCameras"); + + int n_cameras = mPreview.getCameraControllerManager().getNumberOfCameras(); + if( n_cameras <= 1 ) { return; } - int cameraId = mPreview.getCameraId(); - boolean is_front_facing = mPreview.getCameraControllerManager().isFrontFacing(cameraId); + if( test_multi_cam ) { + assertFalse(cycle_all_cameras); + assertTrue(mActivity.isMultiCamEnabled()); + } + + int orig_cameraId = mPreview.getCameraId(); + Set camera_ids = new HashSet<>(); + camera_ids.add(orig_cameraId); + + boolean done_front_test = false; + for(int i=0;i<(cycle_all_cameras ? n_cameras-1 : 1);i++) { + Log.d(TAG, "i: " + i); + int cameraId = mPreview.getCameraId(); + + CameraController.Facing facing = mPreview.getCameraControllerManager().getFacing(cameraId); + if( i == 0 ) { + assertEquals(CameraController.Facing.FACING_BACK, facing); + } + + if( test_multi_cam ) { + // first test cycling through the cameras with this facing + subTestCycleMultiCameras(camera_ids); + } + + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + CharSequence contentDescription = switchCameraButton.getContentDescription(); + clickView(switchCameraButton); + waitUntilCameraOpened(); + + int new_cameraId = mPreview.getCameraId(); + assertTrue(new_cameraId != cameraId); + if( cycle_all_cameras ) { + // in this mode, we should just be iterating over the camera IDs + assertEquals((cameraId + 1) % n_cameras, new_cameraId); + } + assertFalse(camera_ids.contains(new_cameraId)); + camera_ids.add(new_cameraId); + + CameraController.Facing new_facing = mPreview.getCameraControllerManager().getFacing(new_cameraId); + CharSequence new_contentDescription = switchCameraButton.getContentDescription(); + if( n_cameras == 2 || !cycle_all_cameras ) { + assertEquals(facing==CameraController.Facing.FACING_BACK ? CameraController.Facing.FACING_FRONT : CameraController.Facing.FACING_BACK, new_facing); + } + + //int next_cameraId = (new_cameraId+1) % n_cameras; + int next_cameraId = mActivity.getNextCameraId(); + assertTrue(next_cameraId != new_cameraId); + if( cycle_all_cameras ) { + // in this mode, we should just be iterating over the camera IDs + assertEquals((new_cameraId + 1) % n_cameras, next_cameraId); + } + if( i==n_cameras-1 || !cycle_all_cameras ) { + // should have returned to the start + assertEquals(cameraId, next_cameraId); + } + CameraController.Facing next_facing = mPreview.getCameraControllerManager().getFacing(next_cameraId); + if( n_cameras == 2 || !cycle_all_cameras ) { + assertEquals(facing, next_facing); + } + + Log.d(TAG, "cameraId: " + cameraId); + Log.d(TAG, "facing: " + facing); + Log.d(TAG, "contentDescription: " + contentDescription); + Log.d(TAG, "new_cameraId: " + new_cameraId); + Log.d(TAG, "new_facing: " + new_facing); + Log.d(TAG, "new_contentDescription: " + new_contentDescription); + Log.d(TAG, "next_cameraId: " + next_cameraId); + Log.d(TAG, "next_facing: " + next_facing); + + switch( new_facing ) { + case FACING_FRONT: + assertEquals(contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_front_camera)); + break; + case FACING_BACK: + assertEquals(contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_back_camera)); + break; + case FACING_EXTERNAL: + assertEquals(contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_external_camera)); + break; + default: + fail(); + } + switch( next_facing ) { + case FACING_FRONT: + assertEquals(new_contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_front_camera)); + break; + case FACING_BACK: + assertEquals(new_contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_back_camera)); + break; + case FACING_EXTERNAL: + assertEquals(new_contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_external_camera)); + break; + default: + fail(); + } + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + if( !done_front_test && new_facing == CameraController.Facing.FACING_FRONT ) { + done_front_test = true; + + // check still front camera after pause/resume + pauseAndResume(); + + int restart_cameraId = mPreview.getCameraId(); + CharSequence restart_contentDescription = switchCameraButton.getContentDescription(); + Log.d(TAG, "restart_contentDescription: " + restart_contentDescription); + assertEquals(restart_cameraId, new_cameraId); + switch( next_facing ) { + case FACING_FRONT: + assertEquals(restart_contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_front_camera)); + break; + case FACING_BACK: + assertEquals(restart_contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_back_camera)); + break; + case FACING_EXTERNAL: + assertEquals(restart_contentDescription, mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_external_camera)); + break; + default: + fail(); + } + + // now test mirror mode + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.FrontCameraMirrorKey, "preference_front_camera_mirror_photo"); + editor.apply(); + updateForSettings(); + subTestTakePhoto(false, false, true, true, false, false, false, false); + // disable mirror mode again + settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + editor = settings.edit(); + editor.putString(PreferenceKeys.FrontCameraMirrorKey, "preference_front_camera_mirror_no"); + editor.apply(); + updateForSettings(); + } + } + + if( test_multi_cam ) { + subTestCycleMultiCameras(camera_ids); + } + + if( cycle_all_cameras || test_multi_cam ) { + // test we visited all cameras + assertEquals(n_cameras, camera_ids.size()); + } + + // now check we really do return to the first camera View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); - CharSequence contentDescription = switchCameraButton.getContentDescription(); clickView(switchCameraButton); waitUntilCameraOpened(); - int new_cameraId = mPreview.getCameraId(); - assertTrue(new_cameraId != cameraId); - boolean new_is_front_facing = mPreview.getCameraControllerManager().isFrontFacing(new_cameraId); - CharSequence new_contentDescription = switchCameraButton.getContentDescription(); + int final_cameraId = mPreview.getCameraId(); + assertEquals(orig_cameraId, final_cameraId); + } - Log.d(TAG, "cameraId: " + cameraId); - Log.d(TAG, "is_front_facing: " + is_front_facing); - Log.d(TAG, "contentDescription: " + contentDescription); - Log.d(TAG, "new_cameraId: " + new_cameraId); - Log.d(TAG, "new_is_front_facing: " + new_is_front_facing); - Log.d(TAG, "new_contentDescription: " + new_contentDescription); + /* Tests taking a photo with all non-default cameras. + * For multi-camera devices, this tests the behaviour with + * PreferenceKeys.MultiCamButtonPreferenceKey devices, so the switch camera icon still cycles + * through all cameras. + */ + public void testTakePhotoFrontCameraAll() throws InterruptedException { + Log.d(TAG, "testTakePhotoFrontCameraAll"); + setToDefault(); - assertTrue(cameraId != new_cameraId); - assertTrue( contentDescription.equals( mActivity.getResources().getString(new_is_front_facing ? net.sourceforge.opencamera.R.string.switch_to_front_camera : net.sourceforge.opencamera.R.string.switch_to_back_camera) ) ); - assertTrue( new_contentDescription.equals( mActivity.getResources().getString(is_front_facing ? net.sourceforge.opencamera.R.string.switch_to_front_camera : net.sourceforge.opencamera.R.string.switch_to_back_camera) ) ); - subTestTakePhoto(false, false, true, true, false, false, false, false); + if( mActivity.isMultiCamEnabled() ) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.MultiCamButtonPreferenceKey, false); + editor.apply(); + updateForSettings(); + } - // check still front camera after pause/resume - pauseAndResume(); + subTestTakePhotoMultiCameras(true, false); + } - int restart_cameraId = mPreview.getCameraId(); - CharSequence restart_contentDescription = switchCameraButton.getContentDescription(); - Log.d(TAG, "restart_contentDescription: " + restart_contentDescription); - assertTrue(restart_cameraId == new_cameraId); - assertTrue( restart_contentDescription.equals( mActivity.getResources().getString(is_front_facing ? net.sourceforge.opencamera.R.string.switch_to_front_camera : net.sourceforge.opencamera.R.string.switch_to_back_camera) ) ); + /* Tests taking a photo on multi-camera devices with front and back cameras. + */ + public void testTakePhotoFrontCamera() throws InterruptedException { + Log.d(TAG, "testTakePhotoFrontCamera"); + setToDefault(); - // now test mirror mode - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); - SharedPreferences.Editor editor = settings.edit(); - editor.putString(PreferenceKeys.FrontCameraMirrorKey, "preference_front_camera_mirror_photo"); - editor.apply(); - updateForSettings(); - subTestTakePhoto(false, false, true, true, false, false, false, false); + if( !mActivity.isMultiCamEnabled() ) { + return; // no point running, as will be same as testTakePhotoFrontCameraAll + } + + subTestTakePhotoMultiCameras(false, false); + } + + /* Tests taking a photo on multi-camera devices, using both icons to switch between cameras. + */ + public void testTakePhotoFrontCameraMulti() throws InterruptedException { + Log.d(TAG, "testTakePhotoFrontCameraMulti"); + setToDefault(); + + if( !mActivity.isMultiCamEnabled() ) { + return; + } + + subTestTakePhotoMultiCameras(false, true); } /* Tests taking a photo with front camera and screen flash. - * And tests that we save the current camera when pausing and resuming. */ public void testTakePhotoFrontCameraScreenFlash() throws InterruptedException { Log.d(TAG, "testTakePhotoFrontCameraScreenFlash"); @@ -4124,7 +4594,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.VISIBLE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); // now wait for immersive mode to kick in Thread.sleep(6000); - assertTrue(switchCameraButton.getVisibility() == View.GONE); - assertTrue(switchVideoButton.getVisibility() == View.GONE); - assertTrue(exposureButton.getVisibility() == View.GONE); - assertTrue(exposureLockButton.getVisibility() == View.GONE); - assertTrue(audioControlButton.getVisibility() == View.GONE); - assertTrue(popupButton.getVisibility() == View.GONE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.GONE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), View.GONE); + assertEquals(switchMultiCameraButton.getVisibility(), View.GONE); + assertEquals(switchVideoButton.getVisibility(), View.GONE); + assertEquals(exposureButton.getVisibility(), View.GONE); + assertEquals(exposureLockButton.getVisibility(), View.GONE); + assertEquals(audioControlButton.getVisibility(), View.GONE); + assertEquals(popupButton.getVisibility(), View.GONE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.GONE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); subTestTakePhoto(false, true, true, true, false, false, false, false); // test now exited immersive mode - assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); - assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), (mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.VISIBLE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); // wait for immersive mode to kick in again Thread.sleep(6000); - assertTrue(switchCameraButton.getVisibility() == View.GONE); - assertTrue(switchVideoButton.getVisibility() == View.GONE); - assertTrue(exposureButton.getVisibility() == View.GONE); - assertTrue(exposureLockButton.getVisibility() == View.GONE); - assertTrue(audioControlButton.getVisibility() == View.GONE); - assertTrue(popupButton.getVisibility() == View.GONE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.GONE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), View.GONE); + assertEquals(switchMultiCameraButton.getVisibility(), View.GONE); + assertEquals(switchVideoButton.getVisibility(), View.GONE); + assertEquals(exposureButton.getVisibility(), View.GONE); + assertEquals(exposureLockButton.getVisibility(), View.GONE); + assertEquals(audioControlButton.getVisibility(), View.GONE); + assertEquals(popupButton.getVisibility(), View.GONE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.GONE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); subTestTakePhotoPreviewPaused(true, false); // test now exited immersive mode - assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); - assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), (mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.VISIBLE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); // need to switch video before going back to immersive mode if( !mPreview.isVideo() ) { @@ -4340,50 +4816,53 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.VISIBLE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); // wait for immersive mode to kick in again Thread.sleep(6000); - assertTrue(switchCameraButton.getVisibility() == View.GONE); - assertTrue(switchVideoButton.getVisibility() == View.GONE); - assertTrue(exposureButton.getVisibility() == View.GONE); - assertTrue(exposureLockButton.getVisibility() == View.GONE); - assertTrue(audioControlButton.getVisibility() == View.GONE); - assertTrue(popupButton.getVisibility() == View.GONE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.GONE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); - - subTestTakeVideo(false, false, false, true, null, 5000, false, false); + assertEquals(switchCameraButton.getVisibility(), View.GONE); + assertEquals(switchMultiCameraButton.getVisibility(), View.GONE); + assertEquals(switchVideoButton.getVisibility(), View.GONE); + assertEquals(exposureButton.getVisibility(), View.GONE); + assertEquals(exposureLockButton.getVisibility(), View.GONE); + assertEquals(audioControlButton.getVisibility(), View.GONE); + assertEquals(popupButton.getVisibility(), View.GONE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.GONE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); + + subTestTakeVideo(false, false, false, true, null, 5000, false, 0); // test touch exits immersive mode TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); - assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); - assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), (mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.VISIBLE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); // switch back to photo mode if( mPreview.isVideo() ) { @@ -4397,18 +4876,19 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.VISIBLE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); // now wait for immersive mode to kick in Thread.sleep(6000); - assertTrue(switchCameraButton.getVisibility() == View.GONE); - assertTrue(switchVideoButton.getVisibility() == View.GONE); - assertTrue(exposureButton.getVisibility() == View.GONE); - assertTrue(exposureLockButton.getVisibility() == View.GONE); - assertTrue(popupButton.getVisibility() == View.GONE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.GONE); - assertTrue(takePhotoButton.getVisibility() == View.GONE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), View.GONE); + assertEquals(switchMultiCameraButton.getVisibility(), View.GONE); + assertEquals(switchVideoButton.getVisibility(), View.GONE); + assertEquals(exposureButton.getVisibility(), View.GONE); + assertEquals(exposureLockButton.getVisibility(), View.GONE); + assertEquals(popupButton.getVisibility(), View.GONE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.GONE); + assertEquals(takePhotoButton.getVisibility(), View.GONE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); // now touch to exit immersive mode TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); Thread.sleep(500); // test now exited immersive mode - assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); - assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), (mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(zoomSeekBar.getVisibility(), View.VISIBLE); + assertEquals(takePhotoButton.getVisibility(), View.VISIBLE); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); + } - // test touch exits immersive mode - TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); - assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); - assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); - assertTrue(takePhotoButton.getVisibility() == View.VISIBLE); - assertTrue(pauseVideoButton.getVisibility() == View.GONE); - assertTrue(takePhotoVideoButton.getVisibility() == View.GONE); + /** Tests the use of the FLAG_LAYOUT_NO_LIMITS flag introduced in 1.48. + */ + public void testLayoutNoLimits() throws InterruptedException { + Log.d(TAG, "testLayoutNoLimits"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) { + // we don't support FLAG_LAYOUT_NO_LIMITS + return; + } + + MainActivity.test_preview_want_no_limits = true; + MainActivity.test_preview_want_no_limits_value = false; + // need to restart for test_preview_want_no_limits static to take effect + restart(); + + setToDefault(); + + Thread.sleep(1000); + assertEquals(0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(0, mActivity.getMainUI().test_navigation_gap); + + // test changing resolution + MainActivity.test_preview_want_no_limits_value = true; + updateForSettings(); + Thread.sleep(1000); + boolean supports_no_limits = mActivity.getNavigationGap() != 0; + Log.d(TAG, "supports_no_limits: " + supports_no_limits); + + assertEquals(supports_no_limits ? WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS : 0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(mActivity.getNavigationGap(), mActivity.getMainUI().test_navigation_gap); + MainActivity.test_preview_want_no_limits_value = false; + updateForSettings(); + Thread.sleep(1000); + assertEquals(0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(0, mActivity.getMainUI().test_navigation_gap); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + // test switching camera + MainActivity.test_preview_want_no_limits_value = true; + switchToCamera(1); + Thread.sleep(1000); + assertEquals(supports_no_limits ? WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS : 0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(mActivity.getNavigationGap(), mActivity.getMainUI().test_navigation_gap); + MainActivity.test_preview_want_no_limits_value = false; + switchToCamera(0); + Thread.sleep(1000); + assertEquals(0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(0, mActivity.getMainUI().test_navigation_gap); + } + + // test switching to video and back + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + MainActivity.test_preview_want_no_limits_value = true; + clickView(switchVideoButton); + waitUntilCameraOpened(); + assertTrue(mPreview.isVideo()); + Thread.sleep(1000); + assertEquals(supports_no_limits ? WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS : 0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(mActivity.getNavigationGap(), mActivity.getMainUI().test_navigation_gap); + MainActivity.test_preview_want_no_limits_value = false; + clickView(switchVideoButton); + waitUntilCameraOpened(); + assertFalse(mPreview.isVideo()); + Thread.sleep(1000); + assertEquals(0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(0, mActivity.getMainUI().test_navigation_gap); + + // test after restart + MainActivity.test_preview_want_no_limits_value = true; + restart(); + Thread.sleep(1000); + assertEquals(supports_no_limits ? WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS : 0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(mActivity.getNavigationGap(), mActivity.getMainUI().test_navigation_gap); + } + + /** Tests the use of the FLAG_LAYOUT_NO_LIMITS flag introduced in 1.48, with the mode set from startup. + */ + public void testLayoutNoLimitsStartup() throws InterruptedException { + Log.d(TAG, "testLayoutNoLimitsStartup"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) { + // we don't support FLAG_LAYOUT_NO_LIMITS + return; + } + + MainActivity.test_preview_want_no_limits = true; + MainActivity.test_preview_want_no_limits_value = true; + // need to restart for test_preview_want_no_limits static to take effect + restart(); + + setToDefault(); + + Thread.sleep(1000); + boolean supports_no_limits = mActivity.getNavigationGap() != 0; + Log.d(TAG, "supports_no_limits: " + supports_no_limits); + + Log.d(TAG, "check FLAG_LAYOUT_NO_LIMITS"); + Log.d(TAG, "test_navigation_gap: " + mActivity.getMainUI().test_navigation_gap); + assertEquals(supports_no_limits ? WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS : 0, mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + assertEquals(mActivity.getNavigationGap(), mActivity.getMainUI().test_navigation_gap); } private void subTestTakePhotoPreviewPaused(boolean immersive_mode, boolean is_raw) throws InterruptedException { @@ -4517,6 +5089,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE))); + assertEquals(switchMultiCameraButton.getVisibility(), (immersive_mode ? View.GONE : (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE))); + assertEquals(switchVideoButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); // store status to compare with later int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(audioControlButton.getVisibility() == ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); Log.d(TAG, "about to click take photo"); @@ -4544,29 +5118,30 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); //assertTrue(flashButton.getVisibility() == flashVisibility); //assertTrue(focusButton.getVisibility() == focusVisibility); if( !immersive_mode ) { - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); } - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); // check still same icon even after a delay Log.d(TAG, "thumbnail:" + thumbnail); Log.d(TAG, "mActivity.gallery_bitmap: " + mActivity.gallery_bitmap); - assertTrue(mActivity.gallery_bitmap == thumbnail); + assertSame(mActivity.gallery_bitmap, thumbnail); Thread.sleep(1000); Log.d(TAG, "thumbnail:" + thumbnail); Log.d(TAG, "mActivity.gallery_bitmap: " + mActivity.gallery_bitmap); - assertTrue(mActivity.gallery_bitmap == thumbnail); + assertSame(mActivity.gallery_bitmap, thumbnail); mActivity.waitUntilImageQueueEmpty(); } @@ -4637,8 +5213,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); // flash and focus etc default visibility tested in another test // but store status to compare with later //int flashVisibility = flashButton.getVisibility(); //int focusVisibility = focusButton.getVisibility(); int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); Log.d(TAG, "about to click take photo"); @@ -4698,31 +5276,32 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); //assertTrue(flashButton.getVisibility() == flashVisibility); //assertTrue(focusButton.getVisibility() == focusVisibility); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); // icon may be null, or have been set to another image - only changed after a delay Thread.sleep(2000); Log.d(TAG, "gallery_bitmap: " + mActivity.gallery_bitmap); Log.d(TAG, "thumbnail: " + thumbnail); - assertTrue(mActivity.gallery_bitmap != thumbnail); + assertNotSame(mActivity.gallery_bitmap, thumbnail); } mActivity.waitUntilImageQueueEmpty(); } @@ -4785,8 +5365,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); // flash and focus etc default visibility tested in another test // but store status to compare with later //int flashVisibility = flashButton.getVisibility(); //int focusVisibility = focusButton.getVisibility(); int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); // autofocus shouldn't be immediately, but after a delay Thread.sleep(2000); @@ -5037,13 +5619,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); //assertTrue(flashButton.getVisibility() == flashVisibility); //assertTrue(focusButton.getVisibility() == focusVisibility); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); } private void takePhotoLoop(int count) { @@ -5091,13 +5674,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE))); + assertEquals(switchMultiCameraButton.getVisibility(), (immersive_mode ? View.GONE : (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE))); + assertEquals(switchVideoButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); // but store status to compare with later int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(audioControlButton.getVisibility() == ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); - - assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_selector ); - assertTrue( (Integer)switchVideoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_photo ); - assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ) ); + assertEquals(audioControlButton.getVisibility(), ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + + assertEquals((int) (Integer) takePhotoButton.getTag(), net.sourceforge.opencamera.R.drawable.take_video_selector); + assertEquals((int) (Integer) switchVideoButton.getTag(), net.sourceforge.opencamera.R.drawable.take_photo); + assertEquals(takePhotoButton.getContentDescription(), mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video)); Log.d(TAG, "about to click take video"); clickView(takePhotoButton); Log.d(TAG, "done clicking take video"); this.getInstrumentation().waitForIdleSync(); Log.d(TAG, "after idle sync"); + if( mPreview.usingCamera2API() ) { + assertEquals(mPreview.getCurrentPreviewSize().width, mPreview.getCameraController().test_texture_view_buffer_w); + assertEquals(mPreview.getCurrentPreviewSize().height, mPreview.getCameraController().test_texture_view_buffer_h); + } + if( mPreview.isOnTimer() ) { Log.d(TAG, "wait for timer"); while( mPreview.isOnTimer() ) { @@ -5337,27 +5937,28 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2= Build.VERSION_CODES.N ) - assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertEquals(pauseVideoButton.getVisibility(), View.VISIBLE); else - assertTrue( pauseVideoButton.getVisibility() == View.GONE ); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); if( mPreview.supportsPhotoVideoRecording() ) - assertTrue( takePhotoVideoButton.getVisibility() == View.VISIBLE ); + assertEquals(takePhotoVideoButton.getVisibility(), View.VISIBLE); else - assertTrue( takePhotoVideoButton.getVisibility() == View.GONE ); - assertTrue(switchCameraButton.getVisibility() == View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); + assertEquals(switchCameraButton.getVisibility(), View.GONE); + assertEquals(switchMultiCameraButton.getVisibility(), View.GONE); //assertTrue(switchVideoButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); - assertTrue(switchVideoButton.getVisibility() == View.GONE); - assertTrue(audioControlButton.getVisibility() == View.GONE); - assertTrue(popupButton.getVisibility() == (!immersive_mode && mPreview.supportsFlash() ? View.VISIBLE : View.GONE)); // popup button only visible when recording video if flash supported - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(switchVideoButton.getVisibility(), View.GONE); + assertEquals(audioControlButton.getVisibility(), View.GONE); + assertEquals(popupButton.getVisibility(), (!immersive_mode && mPreview.supportsFlash() ? View.VISIBLE : View.GONE)); // popup button only visible when recording video if flash supported + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); if( test_cb == null ) { if( !immersive_mode && time_ms > 500 ) { @@ -5368,15 +5969,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE))); + assertEquals(switchMultiCameraButton.getVisibility(), (immersive_mode ? View.GONE : (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE))); + assertEquals(audioControlButton.getVisibility(), ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); } - assertTrue(switchVideoButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(popupButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(switchVideoButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(popupButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); assertFalse( mPreview.isVideoRecording() ); - assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_selector ); - assertTrue( (Integer)switchVideoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_photo ); + assertEquals((int) (Integer) takePhotoButton.getTag(), net.sourceforge.opencamera.R.drawable.take_video_selector); + assertEquals((int) (Integer) switchVideoButton.getTag(), net.sourceforge.opencamera.R.drawable.take_photo); assertEquals( takePhotoButton.getContentDescription(), mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ); - assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertEquals(pauseVideoButton.getContentDescription(), mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video)); Log.d(TAG, "pauseVideoButton.getVisibility(): " + pauseVideoButton.getVisibility()); - assertTrue( pauseVideoButton.getVisibility() == View.GONE ); - assertTrue( takePhotoVideoButton.getVisibility() == View.GONE ); + assertEquals(pauseVideoButton.getVisibility(), View.GONE); + assertEquals(takePhotoVideoButton.getVisibility(), View.GONE); + + Log.d(TAG, "test_n_videos_scanned: " + mActivity.getApplicationInterface().test_n_videos_scanned); + if( !allow_failure ) { + assertEquals(n_new_files-n_non_video_files, mActivity.getApplicationInterface().test_n_videos_scanned); + } return n_new_files; } @@ -5488,7 +6097,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 20000 ) { // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi - assertTrue(false); + fail(); } } getInstrumentation().waitForIdleSync(); - assertTrue(mActivity.getLocationSupplier().getLocation() != null); + assertNotNull(mActivity.getLocationSupplier().getLocation()); Log.d(TAG, "have location"); try { @@ -5575,7 +6188,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2= 3000 - time_tol_ms ); @@ -5640,8 +6253,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2= 6000 - time_tol_ms ); @@ -5700,7 +6313,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2= 3000 - time_tol_ms ); @@ -5748,8 +6361,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2= Build.VERSION_CODES.O ) { assertTrue( n_new_files >= 2 ); @@ -6154,9 +6767,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); // flash and focus etc default visibility tested in another test // but store status to compare with later //int flashVisibility = flashButton.getVisibility(); //int focusVisibility = focusButton.getVisibility(); int exposureVisibility = exposureButton.getVisibility(); int exposureLockVisibility = exposureLockButton.getVisibility(); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); // workaround for Android 7.1 bug at https://stackoverflow.com/questions/47548317/what-belong-is-badtokenexception-at-classes-of-project // without this, we get a crash due to that problem on Nexus (old API at least) in testTakeVideoMaxDuration @@ -6817,16 +7609,17 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); //assertTrue(flashButton.getVisibility() == flashVisibility); //assertTrue(focusButton.getVisibility() == focusVisibility); - assertTrue(exposureButton.getVisibility() == exposureVisibility); - assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); - assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); } public void testTakeVideoMaxDuration() throws InterruptedException { @@ -6927,7 +7721,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 supported_flash_values = mPreview.getSupportedFlashValues(); if( supported_flash_values == null ) { // button shouldn't show at all - assertTrue( popupButton.getVisibility() == View.GONE ); + assertEquals(popupButton.getVisibility(), View.GONE); } else { // now open popup again @@ -7661,13 +8455,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), (mPreview.supportsExposures() ? View.VISIBLE : View.GONE)); + assertEquals(exposureLockButton.getVisibility(), (mPreview.supportsExposureLock() ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); clickView(takePhotoButton); waitForTakePhoto(); Log.d(TAG, "done taking 1st photo"); this.getInstrumentation().waitForIdleSync(); - assertTrue(mPreview.count_cameraTakePicture==7); + assertEquals(7, mPreview.count_cameraTakePicture); mActivity.waitUntilImageQueueEmpty(); n_new_files = getNFiles(folder) - n_files; Log.d(TAG, "n_new_files: " + n_new_files); - assertTrue(n_new_files == 7); + assertEquals(7, n_new_files); // wait 2s, should still not have taken another photo Thread.sleep(2000); - assertTrue(mPreview.count_cameraTakePicture==7); + assertEquals(7, mPreview.count_cameraTakePicture); n_new_files = getNFiles(folder) - n_files; Log.d(TAG, "n_new_files: " + n_new_files); - assertTrue(n_new_files == 7); + assertEquals(7, n_new_files); // check GUI has returned to correct state - assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); - assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); - assertTrue(exposureButton.getVisibility() == (mPreview.supportsExposures() ? View.VISIBLE : View.GONE)); - assertTrue(exposureLockButton.getVisibility() == (mPreview.supportsExposureLock() ? View.VISIBLE : View.GONE)); - assertTrue(popupButton.getVisibility() == View.VISIBLE); - assertTrue(trashButton.getVisibility() == View.GONE); - assertTrue(shareButton.getVisibility() == View.GONE); + assertEquals(switchCameraButton.getVisibility(), (mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), (mPreview.supportsExposures() ? View.VISIBLE : View.GONE)); + assertEquals(exposureLockButton.getVisibility(), (mPreview.supportsExposureLock() ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); // wait another 5s, should have taken another photo (need to allow time for the extra auto-focus) Thread.sleep(5000); - assertTrue(mPreview.count_cameraTakePicture==8); + assertEquals(8, mPreview.count_cameraTakePicture); n_new_files = getNFiles(folder) - n_files; Log.d(TAG, "n_new_files: " + n_new_files); - assertTrue(n_new_files == 8); + assertEquals(8, n_new_files); // wait 4s, should not have taken any more photos Thread.sleep(4000); - assertTrue(mPreview.count_cameraTakePicture==8); + assertEquals(8, mPreview.count_cameraTakePicture); n_new_files = getNFiles(folder) - n_files; Log.d(TAG, "n_new_files: " + n_new_files); - assertTrue(n_new_files == 8); + assertEquals(8, n_new_files); } catch(InterruptedException e) { e.printStackTrace(); - assertTrue(false); + fail(); } } @@ -8104,7 +8903,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 20000 ) { // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi - assertTrue(false); + fail(); } } Log.d(TAG, "have received location"); this.getInstrumentation().waitForIdleSync(); - assertTrue(mActivity.getLocationSupplier().getLocation() != null); - assertTrue(mPreview.count_cameraTakePicture==0); + assertNotNull(mActivity.getLocationSupplier().getLocation()); + assertEquals(0, mPreview.count_cameraTakePicture); View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); mActivity.test_last_saved_image = null; @@ -8234,9 +9035,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) { int cameraId = mPreview.getCameraId(); @@ -8368,11 +9196,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 20000 ) { // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi - assertTrue(false); + fail(); } } this.getInstrumentation().waitForIdleSync(); - assertTrue(mActivity.getLocationSupplier().getLocation() != null); + assertNotNull(mActivity.getLocationSupplier().getLocation()); // switch to front camera if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { @@ -8402,7 +9235,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 20000 ) { + // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi + fail(); + } + } + Log.d(TAG, "have received location"); + this.getInstrumentation().waitForIdleSync(); + assertNotNull(mActivity.getLocationSupplier().getLocation()); + // check wasn't cached + LocationSupplier.LocationInfo locationInfo = new LocationSupplier.LocationInfo(); + mActivity.getLocationSupplier().getLocation(locationInfo); + assertFalse(locationInfo.LocationWasCached()); + + // now go to settings + assertFalse(mActivity.isCameraInBackground()); + View settingsButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.settings); + Log.d(TAG, "about to click settings"); + clickView(settingsButton); + Log.d(TAG, "done clicking settings"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertTrue(mActivity.isCameraInBackground()); + + // now check we're not listening for location + start_t = System.currentTimeMillis(); + int count = 0; + while( System.currentTimeMillis() - start_t <= 15000 ) { + assertTrue(mActivity.getLocationSupplier().noLocationListeners()); + assertFalse(mActivity.getLocationSupplier().testHasReceivedLocation()); + assertNull(mActivity.getLocationSupplier().getLocation()); + Thread.sleep(10); + if( count++ == 5 ) { + pauseAndResume(); // check we still don't listen for location after pause and resume + } + } + + // now go back + assertTrue(mActivity.isCameraInBackground()); + Log.d(TAG, "go back"); + mActivity.runOnUiThread(new Runnable() { + public void run() { + mActivity.onBackPressed(); + } + }); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertFalse(mActivity.isCameraInBackground()); + + // check we start listening again + // first should have a cached location + assertTrue(mActivity.getLocationSupplier().hasLocationListeners()); + assertFalse(mActivity.getLocationSupplier().testHasReceivedLocation()); + assertNotNull(mActivity.getLocationSupplier().getLocation()); + locationInfo = new LocationSupplier.LocationInfo(); + mActivity.getLocationSupplier().getLocation(locationInfo); + assertTrue(locationInfo.LocationWasCached()); + + // check we get a non-cached location + while( !mActivity.getLocationSupplier().testHasReceivedLocation() ) { + this.getInstrumentation().waitForIdleSync(); + if( System.currentTimeMillis() - start_t > 20000 ) { + // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi + fail(); + } + } + Log.d(TAG, "have received location"); + this.getInstrumentation().waitForIdleSync(); + assertNotNull(mActivity.getLocationSupplier().getLocation()); + // check wasn't cached + locationInfo = new LocationSupplier.LocationInfo(); + mActivity.getLocationSupplier().getLocation(locationInfo); + assertFalse(locationInfo.LocationWasCached()); + + // now test repeatedly going to settings and back - guard against crash we had where onLocationChanged got called one more time after + // location listeners had been freed + for(int i=0;i<20;i++) { + assertTrue(mActivity.getLocationSupplier().hasLocationListeners()); + Thread.sleep((i % 5) * 100); + + // go to settings + assertFalse(mActivity.isCameraInBackground()); + Log.d(TAG, "about to click settings"); + clickView(settingsButton); + Log.d(TAG, "done clicking settings"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertTrue(mActivity.isCameraInBackground()); + + Thread.sleep(100); + assertTrue(mActivity.getLocationSupplier().noLocationListeners()); + assertFalse(mActivity.getLocationSupplier().testHasReceivedLocation()); + assertNull(mActivity.getLocationSupplier().getLocation()); + + Thread.sleep(200); + assertTrue(mActivity.getLocationSupplier().noLocationListeners()); + assertFalse(mActivity.getLocationSupplier().testHasReceivedLocation()); + assertNull(mActivity.getLocationSupplier().getLocation()); + + // go back + assertTrue(mActivity.isCameraInBackground()); + Log.d(TAG, "go back"); + mActivity.runOnUiThread(new Runnable() { + public void run() { + mActivity.onBackPressed(); + } + }); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertFalse(mActivity.isCameraInBackground()); + } + } + private void subTestPhotoStamp() throws IOException { { assertFalse(mActivity.getApplicationInterface().getDrawPreview().getStoredHasStampPref()); @@ -8457,7 +9428,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 20000 ) { // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi - assertTrue(false); + fail(); } } this.getInstrumentation().waitForIdleSync(); - assertTrue(mActivity.getLocationSupplier().getLocation() != null); + assertNotNull(mActivity.getLocationSupplier().getLocation()); clickView(takePhotoButton); @@ -8498,9 +9469,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 zoom); - assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + assertEquals(max_zoom - zoomSeekBar.getProgress(), mPreview.getCameraController().getZoom()); mPreview.scaleZoom(0.5f); this.getInstrumentation().waitForIdleSync(); Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + zoom); - assertTrue(mPreview.getCameraController().getZoom() == zoom); - assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + assertEquals(mPreview.getCameraController().getZoom(), zoom); + assertEquals(max_zoom - zoomSeekBar.getProgress(), mPreview.getCameraController().getZoom()); // test to max/min mPreview.scaleZoom(10000.0f); this.getInstrumentation().waitForIdleSync(); Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to max_zoom " + max_zoom); - assertTrue(mPreview.getCameraController().getZoom() == max_zoom); - assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + assertEquals(mPreview.getCameraController().getZoom(), max_zoom); + assertEquals(max_zoom - zoomSeekBar.getProgress(), mPreview.getCameraController().getZoom()); mPreview.scaleZoom(1.0f/10000.0f); this.getInstrumentation().waitForIdleSync(); Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zero"); - assertTrue(mPreview.getCameraController().getZoom() == 0); - assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + assertEquals(0, mPreview.getCameraController().getZoom()); + assertEquals(max_zoom - zoomSeekBar.getProgress(), mPreview.getCameraController().getZoom()); // use seekbar to zoom Log.d(TAG, "zoom to max"); @@ -8708,13 +9679,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 0); assertTrue(save_location_history.contains(save_folder)); - assertTrue(save_location_history.get( save_location_history.size()-1 ).equals(save_folder)); + assertEquals(save_location_history.get(save_location_history.size() - 1), save_folder); File folder = mActivity.getImageFolder(); if( folder.exists() && delete_folder ) { @@ -9103,11 +10074,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2= CameraController.ISO_FOR_DARK ) + assertEquals(CameraController.N_IMAGES_NR_DARK_LOW_LIGHT, mActivity.getPreview().getCameraController().getBurstTotal()); + // reset + mActivity.getApplicationInterface().setNRMode("preference_nr_mode_normal"); + mActivity.getPreview().setupBurstMode(); + // then try front camera if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { @@ -10401,7 +11393,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), View.VISIBLE); + assertEquals(exposureLockButton.getVisibility(), View.VISIBLE); + assertEquals(audioControlButton.getVisibility(), View.GONE); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(settingsButton.getVisibility(), View.VISIBLE); + assertEquals(cancelPanoramaButton.getVisibility(), View.GONE); Log.d(TAG, "about to click take photo"); clickView(takePhotoButton); @@ -10579,23 +11574,24 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + assertEquals(exposureButton.getVisibility(), View.VISIBLE); + assertEquals(exposureLockButton.getVisibility(), View.VISIBLE); + assertEquals(audioControlButton.getVisibility(), View.GONE); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + assertEquals(settingsButton.getVisibility(), View.VISIBLE); + assertEquals(cancelPanoramaButton.getVisibility(), View.GONE); if( !cancel && !to_max ) { // test trying to take another photo whilst saving @@ -10679,7 +11676,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs, String output_name, String gyro_debug_info_filename, float panorama_pics_per_screen, float camera_angle_x, float camera_angle_y, float gyro_tol_degrees) throws IOException, InterruptedException { Log.d(TAG, "subTestPanorama"); @@ -15582,9 +16311,28 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>(); + + inputs.add(panorama_images_path + "testPanoramaWhite/input0.jpg"); + inputs.add(panorama_images_path + "testPanoramaWhite/input0.jpg"); + float camera_angle_x = 66.3177f; + float camera_angle_y = 50.04736f; + float panorama_pics_per_screen = 2.0f; + String output_name = "testPanoramaWhite_output.jpg"; + + subTestPanorama(inputs, output_name, null, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 2.0f); + } + /** Tests panorama algorithm on test samples "testPanorama1". - * @throws IOException - * @throws InterruptedException */ public void testPanorama1() throws IOException, InterruptedException { Log.d(TAG, "testPanorama1"); @@ -15609,8 +16357,6 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 + tools:ignore="GoogleAppIndexingWarning"> + @@ -18,7 +20,7 @@ - + bitmaps, boolean release_bitmaps, Bitmap output_bitmap, float hdr_alpha, int n_tiles, boolean ce_preserve_blacks, DROTonemappingAlgorithm dro_tonemapping_algorithm) throws HDRProcessorException { + private void processSingleImage(List bitmaps, boolean release_bitmaps, Bitmap output_bitmap, float hdr_alpha, int n_tiles, boolean ce_preserve_blacks, DROTonemappingAlgorithm dro_tonemapping_algorithm) { if( MyDebug.LOG ) Log.d(TAG, "processSingleImage"); @@ -1263,10 +1265,9 @@ public class HDRProcessor { * @param bitmap_avg_align Should be supplied if allocation_avg_align is non-null, and stores * the bitmap corresponding to the allocation_avg_align. * @param time_s Time, for debugging. - * @throws HDRProcessorException */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private AvgData processAvgCore(Allocation allocation_out, Allocation allocation_avg, Bitmap bitmap_avg, Bitmap bitmap_new, int width, int height, float avg_factor, int iso, float zoom_factor, Allocation allocation_avg_align, Bitmap bitmap_avg_align, long time_s) throws HDRProcessorException { + private AvgData processAvgCore(Allocation allocation_out, Allocation allocation_avg, Bitmap bitmap_avg, Bitmap bitmap_new, int width, int height, float avg_factor, int iso, float zoom_factor, Allocation allocation_avg_align, Bitmap bitmap_avg_align, long time_s) { if( MyDebug.LOG ) { Log.d(TAG, "processAvgCore"); Log.d(TAG, "iso: " + iso); @@ -1399,10 +1400,12 @@ public class HDRProcessor { if( bitmap_new_align != null ) { bitmap_new_align.recycle(); + //noinspection UnusedAssignment bitmap_new_align = null; } if( allocation_new_align != null ) { allocation_new_align.destroy(); + //noinspection UnusedAssignment allocation_new_align = null; } @@ -1539,12 +1542,14 @@ public class HDRProcessor { if( MyDebug.LOG ) Log.d(TAG, "release bitmap_avg"); bitmap_avg.recycle(); + //noinspection UnusedAssignment bitmap_avg = null; } if( bitmap_new != null ) { if( MyDebug.LOG ) Log.d(TAG, "release bitmap_new"); bitmap_new.recycle(); + //noinspection UnusedAssignment bitmap_new = null; } @@ -2664,7 +2669,7 @@ public class HDRProcessor { int max_brightness = 0; for(int i = 0; i < histo.length; i++) { count += histo[i]; - sum_brightness += (double)(histo[i] * i); + sum_brightness += (histo[i] * i); if( count >= middle && median_brightness == -1 ) { median_brightness = i; } @@ -3026,6 +3031,7 @@ public class HDRProcessor { * @param allocation_in The input allocation. * @param width The width of the allocation. */ + @SuppressWarnings("unused") @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private float computeSharpness(Allocation allocation_in, int width, long time_s) { if( MyDebug.LOG ) diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessorException.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessorException.java index 70f7a9b4359c83f63936a232e299683ca23df87a..056e288dbc8740ba8b224dca9d26c23350c17eb1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessorException.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessorException.java @@ -2,6 +2,7 @@ package net.sourceforge.opencamera; /** Exception for HDRProcessor class. */ +@SuppressWarnings("WeakerAccess") public class HDRProcessorException extends Exception { final static public int INVALID_N_IMAGES = 0; // the supplied number of images is not supported final static public int UNEQUAL_SIZES = 1; // images not of the same resolution diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 54d304abcfa3239536437a92de770984ae896830..0e2a2561ca6c17af5e7f12ce8e9782cd4a3e2a1f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -140,7 +140,7 @@ public class ImageSaver extends Thread { ImageFormat image_format; int image_quality; boolean do_auto_stabilise; - final double level_angle; + final double level_angle; // in degrees final List gyro_rotation_matrix; // used for panorama (one 3x3 matrix per jpeg_images entry), otherwise can be null boolean panorama_dir_left_to_right; // used for panorama float camera_view_angle_x; // used for panorama @@ -166,7 +166,9 @@ public class ImageSaver extends Thread { final boolean store_location; final Location location; final boolean store_geo_direction; - final double geo_direction; + final double geo_direction; // in radians + final boolean store_ypr; // whether to store geo_angle, pitch_angle, level_angle in USER_COMMENT if exif (for JPEGs) + final double pitch_angle; // the pitch that the phone is at, in degrees final String custom_tag_artist; final String custom_tag_copyright; final int sample_factor; // sampling factor for thumbnail, higher means lower quality @@ -192,6 +194,7 @@ public class ImageSaver extends Thread { String preference_stamp, String preference_textstamp, int font_size, int color, String pref_style, String preference_stamp_dateformat, String preference_stamp_timeformat, String preference_stamp_gpsformat, String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, + double pitch_angle, boolean store_ypr, String custom_tag_artist, String custom_tag_copyright, int sample_factor) { @@ -232,6 +235,8 @@ public class ImageSaver extends Thread { this.location = location; this.store_geo_direction = store_geo_direction; this.geo_direction = geo_direction; + this.pitch_angle = pitch_angle; + this.store_ypr = store_ypr; this.custom_tag_artist = custom_tag_artist; this.custom_tag_copyright = custom_tag_copyright; this.sample_factor = sample_factor; @@ -261,6 +266,7 @@ public class ImageSaver extends Thread { this.zoom_factor, this.preference_stamp, this.preference_textstamp, this.font_size, this.color, this.pref_style, this.preference_stamp_dateformat, this.preference_stamp_timeformat, this.preference_stamp_gpsformat, this.preference_stamp_geo_address, this.preference_units_distance, this.panorama_crop, this.store_location, this.location, this.store_geo_direction, this.geo_direction, + this.pitch_angle, this.store_ypr, this.custom_tag_artist, this.custom_tag_copyright, this.sample_factor); @@ -552,6 +558,7 @@ public class ImageSaver extends Thread { String preference_stamp, String preference_textstamp, int font_size, int color, String pref_style, String preference_stamp_dateformat, String preference_stamp_timeformat, String preference_stamp_gpsformat, String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, + double pitch_angle, boolean store_ypr, String custom_tag_artist, String custom_tag_copyright, int sample_factor) { @@ -581,6 +588,7 @@ public class ImageSaver extends Thread { zoom_factor, preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, preference_stamp_geo_address, preference_units_distance, panorama_crop, store_location, location, store_geo_direction, geo_direction, + pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, sample_factor); @@ -622,6 +630,7 @@ public class ImageSaver extends Thread { 1.0f, null, null, 0, 0, null, null, null, null, null, null, false, false, null, false, 0.0, + 0.0, false, null, null, 1); } @@ -647,6 +656,7 @@ public class ImageSaver extends Thread { String preference_stamp, String preference_textstamp, int font_size, int color, String pref_style, String preference_stamp_dateformat, String preference_stamp_timeformat, String preference_stamp_gpsformat, String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, + double pitch_angle, boolean store_ypr, String custom_tag_artist, String custom_tag_copyright, int sample_factor) { @@ -674,6 +684,7 @@ public class ImageSaver extends Thread { zoom_factor, preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, preference_stamp_geo_address, preference_units_distance, panorama_crop, store_location, location, store_geo_direction, geo_direction, + pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, sample_factor); @@ -753,6 +764,7 @@ public class ImageSaver extends Thread { String preference_stamp, String preference_textstamp, int font_size, int color, String pref_style, String preference_stamp_dateformat, String preference_stamp_timeformat, String preference_stamp_gpsformat, String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, + double pitch_angle, boolean store_ypr, String custom_tag_artist, String custom_tag_copyright, int sample_factor) { @@ -784,6 +796,7 @@ public class ImageSaver extends Thread { zoom_factor, preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, preference_stamp_geo_address, preference_units_distance, panorama_crop, store_location, location, store_geo_direction, geo_direction, + pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, sample_factor); @@ -894,6 +907,7 @@ public class ImageSaver extends Thread { 1.0f, null, null, 0, 0, null, null, null, null, null, null, false, false, null, false, 0.0, + 0.0, false, null, null, 1); if( MyDebug.LOG ) @@ -1163,7 +1177,9 @@ public class ImageSaver extends Thread { xmlSerializer.flush(); } + @SuppressWarnings("WeakerAccess") public static class GyroDebugInfo { + @SuppressWarnings("unused") public static class GyroImageDebugInfo { public float [] vectorRight; // X axis public float [] vectorUp; // Y axis @@ -1236,6 +1252,7 @@ public class ImageSaver extends Thread { Log.d(TAG, "end tag, name: " + name); } + //noinspection SwitchStatementWithTooFewBranches switch( name ) { case gyro_info_image_tag: image_info = null; @@ -1437,6 +1454,7 @@ public class ImageSaver extends Thread { Log.d(TAG, "*** time for brighten: " + (System.currentTimeMillis() - this_time_s)); } avg_data.destroy(); + //noinspection UnusedAssignment avg_data = null; if( MyDebug.LOG ) { Log.d(TAG, "*** total time for saving NR image: " + (System.currentTimeMillis() - time_s)); @@ -1617,6 +1635,7 @@ public class ImageSaver extends Thread { outputStream = main_activity.getContentResolver().openOutputStream(saveUri); try { //outputStream.write(gyro_text.toString().getBytes()); + //noinspection CharsetObjectCanBeUsed outputStream.write(writer.toString().getBytes(Charset.forName("UTF-8"))); } finally { @@ -1799,6 +1818,67 @@ public class ImageSaver extends Thread { } } + /** Computes the width and height of a centred crop region after having rotated an image. + * @param result - Array of length 2 which will be filled with the returned width and height. + * @param level_angle_rad_abs - Absolute value of angle of rotation, in radians. + * @param w0 - Rotated width. + * @param h0 - Rotated height. + * @param w1 - Original width. + * @param h1 - Original height. + * @param max_width - Maximum width to return. + * @param max_height - Maximum height to return. + * @return - Whether a crop region could be successfully calculated. + */ + public static boolean autoStabiliseCrop(int [] result, double level_angle_rad_abs, double w0, double h0, int w1, int h1, int max_width, int max_height) { + boolean ok = false; + result[0] = 0; + result[1] = 0; + + double tan_theta = Math.tan(level_angle_rad_abs); + double sin_theta = Math.sin(level_angle_rad_abs); + double denom = ( h0/w0 + tan_theta ); + double alt_denom = ( w0/h0 + tan_theta ); + if( denom == 0.0 || denom < 1.0e-14 ) { + if( MyDebug.LOG ) + Log.d(TAG, "zero denominator?!"); + } + else if( alt_denom == 0.0 || alt_denom < 1.0e-14 ) { + if( MyDebug.LOG ) + Log.d(TAG, "zero alt denominator?!"); + } + else { + int w2 = (int)(( h0 + 2.0*h1*sin_theta*tan_theta - w0*tan_theta ) / denom); + int h2 = (int)(w2*h0/w0); + int alt_h2 = (int)(( w0 + 2.0*w1*sin_theta*tan_theta - h0*tan_theta ) / alt_denom); + int alt_w2 = (int)(alt_h2*w0/h0); + if( MyDebug.LOG ) { + //Log.d(TAG, "h0 " + h0 + " 2.0*h1*sin_theta*tan_theta " + 2.0*h1*sin_theta*tan_theta + " w0*tan_theta " + w0*tan_theta + " / h0/w0 " + h0/w0 + " tan_theta " + tan_theta); + Log.d(TAG, "w2 = " + w2 + " , h2 = " + h2); + Log.d(TAG, "alt_w2 = " + alt_w2 + " , alt_h2 = " + alt_h2); + } + if( alt_w2 < w2 ) { + if( MyDebug.LOG ) { + Log.d(TAG, "chose alt!"); + } + w2 = alt_w2; + h2 = alt_h2; + } + if( w2 <= 0 ) + w2 = 1; + else if( w2 > max_width ) + w2 = max_width; + if( h2 <= 0 ) + h2 = 1; + else if( h2 > max_height ) + h2 = max_height; + + ok = true; + result[0] = w2; + result[1] = h2; + } + return ok; + } + /** Performs the auto-stabilise algorithm on the image. * @param data The jpeg data. * @param bitmap Optional argument - the bitmap if already unpacked from the jpeg data. @@ -1895,43 +1975,11 @@ public class ImageSaver extends Thread { Log.d(TAG, "rotated and scaled bitmap size " + bitmap.getWidth() + ", " + bitmap.getHeight()); Log.d(TAG, "rotated and scaled bitmap size: " + bitmap.getWidth()*bitmap.getHeight()*4); } - double tan_theta = Math.tan(level_angle_rad_abs); - double sin_theta = Math.sin(level_angle_rad_abs); - double denom = ( h0/w0 + tan_theta ); - double alt_denom = ( w0/h0 + tan_theta ); - if( denom == 0.0 || denom < 1.0e-14 ) { - if( MyDebug.LOG ) - Log.d(TAG, "zero denominator?!"); - } - else if( alt_denom == 0.0 || alt_denom < 1.0e-14 ) { - if( MyDebug.LOG ) - Log.d(TAG, "zero alt denominator?!"); - } - else { - int w2 = (int)(( h0 + 2.0*h1*sin_theta*tan_theta - w0*tan_theta ) / denom); - int h2 = (int)(w2*h0/w0); - int alt_h2 = (int)(( w0 + 2.0*w1*sin_theta*tan_theta - h0*tan_theta ) / alt_denom); - int alt_w2 = (int)(alt_h2*w0/h0); - if( MyDebug.LOG ) { - //Log.d(TAG, "h0 " + h0 + " 2.0*h1*sin_theta*tan_theta " + 2.0*h1*sin_theta*tan_theta + " w0*tan_theta " + w0*tan_theta + " / h0/w0 " + h0/w0 + " tan_theta " + tan_theta); - Log.d(TAG, "w2 = " + w2 + " , h2 = " + h2); - Log.d(TAG, "alt_w2 = " + alt_w2 + " , alt_h2 = " + alt_h2); - } - if( alt_w2 < w2 ) { - if( MyDebug.LOG ) { - Log.d(TAG, "chose alt!"); - } - w2 = alt_w2; - h2 = alt_h2; - } - if( w2 <= 0 ) - w2 = 1; - else if( w2 >= bitmap.getWidth() ) - w2 = bitmap.getWidth()-1; - if( h2 <= 0 ) - h2 = 1; - else if( h2 >= bitmap.getHeight() ) - h2 = bitmap.getHeight()-1; + + int [] crop = new int [2]; + if( autoStabiliseCrop(crop, level_angle_rad_abs, w0, h0, w1, h1, bitmap.getWidth(), bitmap.getHeight()) ) { + int w2 = crop[0]; + int h2 = crop[1]; int x0 = (bitmap.getWidth()-w2)/2; int y0 = (bitmap.getHeight()-h2)/2; if( MyDebug.LOG ) { @@ -2038,7 +2086,7 @@ public class ImageSaver extends Thread { p.setColor(Color.WHITE); // we don't use the density of the screen, because we're stamping to the image, not drawing on the screen (we don't want the font height to depend on the device's resolution) // instead we go by 1 pt == 1/72 inch height, and scale for an image height (or width if in portrait) of 4" (this means the font height is also independent of the photo resolution) - int smallest_size = (width 0 ) { - if( MyDebug.LOG ) - Log.d(TAG, "stamp with location_string: " + gps_stamp); + // don't log gps_stamp, in case of privacy! Address address = null; if( request.store_location && !request.preference_stamp_geo_address.equals("preference_stamp_geo_address_no") ) { @@ -2105,8 +2152,8 @@ public class ImageSaver extends Thread { List
addresses = geocoder.getFromLocation(request.location.getLatitude(), request.location.getLongitude(), 1); if( addresses != null && addresses.size() > 0 ) { address = addresses.get(0); + // don't log address, in case of privacy! if( MyDebug.LOG ) { - Log.d(TAG, "address: " + address); Log.d(TAG, "max line index: " + address.getMaxAddressLineIndex()); } } @@ -2136,8 +2183,7 @@ public class ImageSaver extends Thread { // we are displaying an address instead of GPS coords, but we still need to display the geo direction gps_stamp = main_activity.getTextFormatter().getGPSString(preference_stamp_gpsformat, request.preference_units_distance, false, null, request.store_geo_direction, request.geo_direction); if( gps_stamp.length() > 0 ) { - if( MyDebug.LOG ) - Log.d(TAG, "gps_stamp is now: " + gps_stamp); + // don't log gps_stamp, in case of privacy! applicationInterface.drawTextWithBackground(canvas, p, gps_stamp, color, Color.BLACK, width - offset_x, ypos, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, null, draw_shadowed); ypos -= diff_y; } @@ -2157,6 +2203,7 @@ public class ImageSaver extends Thread { if( MyDebug.LOG ) Log.d(TAG, "stamp text"); applicationInterface.drawTextWithBackground(canvas, p, request.preference_textstamp, color, Color.BLACK, width - offset_x, ypos, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, null, draw_shadowed); + //noinspection UnusedAssignment ypos -= diff_y; } } @@ -3050,7 +3097,7 @@ public class ImageSaver extends Thread { exif_new.setAttribute(ExifInterface.TAG_USER_COMMENT, exif_user_comment); } - modifyExif(exif_new, request.type == Request.Type.JPEG, request.using_camera2, request.current_date, request.store_location, request.store_geo_direction, request.geo_direction, request.custom_tag_artist, request.custom_tag_copyright); + modifyExif(exif_new, request.type == Request.Type.JPEG, request.using_camera2, request.current_date, request.store_location, request.store_geo_direction, request.geo_direction, request.custom_tag_artist, request.custom_tag_copyright, request.level_angle, request.pitch_angle, request.store_ypr); setDateTimeExif(exif_new); exif_new.saveAttributes(); } @@ -3106,13 +3153,6 @@ public class ImageSaver extends Thread { output = null; success = true; - /*Location location = null; - if( main_activity.getApplicationInterface().getGeotaggingPref() ) { - location = main_activity.getApplicationInterface().getLocation(); - if( MyDebug.LOG ) - Log.d(TAG, "location: " + location); - }*/ - // set last image for share/trash options for pause preview // Must be done before broadcastFile() (because on Android 7+ with non-SAF, we update // the LastImage's uri from the MediaScannerConnection.scanFile() callback from @@ -3129,8 +3169,6 @@ public class ImageSaver extends Thread { } if( saveUri == null ) { - //Uri media_uri = storageUtils.broadcastFileRaw(picFile, current_date, location); - //storageUtils.announceUri(media_uri, true, false); storageUtils.broadcastFile(picFile, true, false, false); } else { @@ -3269,7 +3307,6 @@ public class ImageSaver extends Thread { catch(IOException e) { e.printStackTrace(); } - inputStream = null; } } return bitmap; @@ -3332,14 +3369,14 @@ public class ImageSaver extends Thread { private void updateExif(Request request, File picFile, Uri saveUri) throws IOException { if( MyDebug.LOG ) Log.d(TAG, "updateExif: " + picFile); - if( request.store_geo_direction || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) ) { + if( request.store_geo_direction || request.store_ypr || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) ) { long time_s = System.currentTimeMillis(); if( MyDebug.LOG ) Log.d(TAG, "add additional exif info"); try { ExifInterface exif = createExifInterface(picFile, saveUri); if( exif != null ) { - modifyExif(exif, request.type == Request.Type.JPEG, request.using_camera2, request.current_date, request.store_location, request.store_geo_direction, request.geo_direction, request.custom_tag_artist, request.custom_tag_copyright); + modifyExif(exif, request.type == Request.Type.JPEG, request.using_camera2, request.current_date, request.store_location, request.store_geo_direction, request.geo_direction, request.custom_tag_artist, request.custom_tag_copyright, request.level_angle, request.pitch_angle, request.store_ypr); exif.saveAttributes(); } } @@ -3377,17 +3414,28 @@ public class ImageSaver extends Thread { /** Makes various modifications to the exif data, if necessary. */ - private void modifyExif(ExifInterface exif, boolean is_jpeg, boolean using_camera2, Date current_date, boolean store_location, boolean store_geo_direction, double geo_direction, String custom_tag_artist, String custom_tag_copyright) { + private void modifyExif(ExifInterface exif, boolean is_jpeg, boolean using_camera2, Date current_date, boolean store_location, boolean store_geo_direction, double geo_direction, String custom_tag_artist, String custom_tag_copyright, double level_angle, double pitch_angle, boolean store_ypr) { if( MyDebug.LOG ) Log.d(TAG, "modifyExif"); - setGPSDirectionExif(exif, store_geo_direction, geo_direction); + setGPSDirectionExif(exif, store_geo_direction, geo_direction, level_angle, pitch_angle, store_ypr); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && store_ypr ){ + float geo_angle = (float)Math.toDegrees(geo_direction); + if( geo_angle < 0.0f ) { + geo_angle += 360.0f; + } + String encoding = "ASCII\0\0\0"; + //exif.setAttribute(ExifInterface.TAG_USER_COMMENT,"Yaw:" + geo_angle + ",Pitch:" + pitch_angle + ",Roll:" + level_angle); + exif.setAttribute(ExifInterface.TAG_USER_COMMENT,encoding + "Yaw:" + geo_angle + ",Pitch:" + pitch_angle + ",Roll:" + level_angle); + if( MyDebug.LOG ) + Log.d(TAG, "UserComment: " + exif.getAttribute(ExifInterface.TAG_USER_COMMENT)); + } setCustomExif(exif, custom_tag_artist, custom_tag_copyright); if( needGPSTimestampHack(is_jpeg, using_camera2, store_location) ) { fixGPSTimestamp(exif, current_date); } } - private void setGPSDirectionExif(ExifInterface exif, boolean store_geo_direction, double geo_direction) { + private void setGPSDirectionExif(ExifInterface exif, boolean store_geo_direction, double geo_direction, double level_angle, double pitch_angle, boolean store_ypr) { if( MyDebug.LOG ) Log.d(TAG, "setGPSDirectionExif"); if( store_geo_direction ) { diff --git a/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java b/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java index 1d02f64968ffd9fd416dbea20561ef06ef1fb817..9116546300681067435034549a513251b0e9cfe4 100644 --- a/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java +++ b/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java @@ -24,15 +24,75 @@ public class LocationSupplier { private MyLocationListener [] locationListeners; private volatile boolean test_force_no_location; // if true, always return null location; must be volatile for test project setting the state + private Location cached_location; + private long cached_location_ms; + LocationSupplier(Context context) { this.context = context; locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE); } + private Location getCachedLocation() { + if( cached_location != null ) { + long time_ms = System.currentTimeMillis(); + if( time_ms <= cached_location_ms + 20000 ) { + return cached_location; + } + else { + cached_location = null; + } + } + return null; + } + + /** Cache the current best location. Note that we intentionally call getLocation() from this + * method rather than passing it a location from onLocationChanged(), as we don't want a + * coarse location overriding a better fine location. + */ + private void cacheLocation() { + if( MyDebug.LOG ) + Log.d(TAG, "cacheLocation"); + Location location = getLocation(); + if( location == null ) { + // this isn't an error as it can happen that we receive a call to onLocationChanged() after + // having freed the location listener (possibly because LocationManager had already queued + // a call to onLocationChanged? + // we should not set cached_location to null in such cases + Log.d(TAG, "### asked to cache location when location not available"); + } + else { + cached_location = new Location(location); + cached_location_ms = System.currentTimeMillis(); + } + } + + public static class LocationInfo { + private boolean location_was_cached; + + public boolean LocationWasCached() { + return location_was_cached; + } + } + public Location getLocation() { - // returns null if not available - if( locationListeners == null ) + return getLocation(null); + } + + /** If adding extra calls to this, consider whether explicit user permission is required, and whether + * privacy policy needs updating. + * @param locationInfo Optional class to return additional information about the location. + * @return Returns null if location not available. + */ + public Location getLocation(LocationInfo locationInfo) { + if( locationInfo != null ) + locationInfo.location_was_cached = false; // init + + if( locationListeners == null ) { + // if we have disabled location listening, then don't return a cached location anyway - + // in theory, callers should have already checked for user permission/setting before calling + // getLocation(), but just in case we didn't, don't want to return a cached location return null; + } if( test_force_no_location ) return null; // location listeners should be stored in order best to worst @@ -41,10 +101,13 @@ public class LocationSupplier { if( location != null ) return location; } - return null; + Location location = getCachedLocation(); + if( location != null && locationInfo != null ) + locationInfo.location_was_cached = true; + return location; } - private static class MyLocationListener implements LocationListener { + private class MyLocationListener implements LocationListener { private Location location; volatile boolean test_has_received_location; // must be volatile for test project reading the state @@ -64,6 +127,7 @@ public class LocationSupplier { Log.d(TAG, "lat " + location.getLatitude() + " long " + location.getLongitude() + " accuracy " + location.getAccuracy()); } this.location = location; + cacheLocation(); } } @@ -80,6 +144,7 @@ public class LocationSupplier { } this.location = null; this.test_has_received_location = false; + cached_location = null; break; } default: @@ -95,16 +160,19 @@ public class LocationSupplier { Log.d(TAG, "onProviderDisabled"); this.location = null; this.test_has_received_location = false; + cached_location = null; } } - // returns false if location permission not available for either coarse or fine + /* Best to only call this from MainActivity.initLocation(). + * @return Returns false if location permission not available for either coarse or fine. + */ boolean setupLocationListener() { if( MyDebug.LOG ) Log.d(TAG, "setupLocationListener"); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); // Define a listener that responds to location updates - // we only set it up if store_location is true, to avoid unnecessarily wasting battery + // we only set it up if store_location is true, important for privacy and unnecessary battery use boolean store_location = sharedPreferences.getBoolean(PreferenceKeys.LocationPreferenceKey, false); if( store_location && locationListeners == null ) { // Note, ContextCompat.checkSelfPermission is meant to handle being called on any Android version, i.e., pre @@ -189,6 +257,8 @@ public class LocationSupplier { locationListeners[i] = null; } locationListeners = null; + if( MyDebug.LOG ) + Log.d(TAG, "location listeners now freed"); } } @@ -208,6 +278,9 @@ public class LocationSupplier { this.test_force_no_location = test_force_no_location; } + /** Use this when we want to test (assert) that location listeners are turned on. + * If we want to assert that they are turned off, then use noLocationListeners. + */ public boolean hasLocationListeners() { if( this.locationListeners == null ) return false; @@ -220,6 +293,18 @@ public class LocationSupplier { return true; } + /** Use this when we want to test (assert) that location listeners are turned on. Note that this + * is NOT an inverse of hasLocationListeners. For example this means that if + * locationListeners.length==1, hasLocationListeners would return false (so we'd flag up that + * we've not set them up correctly), but noLocationListeners would also return false (to flag + * up that we did set some location listeners up). + */ + public boolean noLocationListeners() { + if( this.locationListeners == null ) + return true; + return false; + } + public static String locationToDMS(double coord) { String sign = (coord < 0.0) ? "-" : ""; coord = Math.abs(coord); diff --git a/app/src/main/java/net/sourceforge/opencamera/MagneticSensor.java b/app/src/main/java/net/sourceforge/opencamera/MagneticSensor.java index d5392a6c83841b17717650674f57ddaeb8fdc896..d8b7dcbadb174af474a5c6b19e9daac9311fc6ec 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MagneticSensor.java +++ b/app/src/main/java/net/sourceforge/opencamera/MagneticSensor.java @@ -200,6 +200,7 @@ class MagneticSensor { */ private boolean needsMagneticSensor(SharedPreferences sharedPreferences) { if( main_activity.getApplicationInterface().getGeodirectionPref() || + sharedPreferences.getBoolean(PreferenceKeys.AddYPRToComments, false) || sharedPreferences.getBoolean(PreferenceKeys.ShowGeoDirectionLinesPreferenceKey, false) || sharedPreferences.getBoolean(PreferenceKeys.ShowGeoDirectionPreferenceKey, false) ) { return true; diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 472873f12003fece8c1ad21ce2cf1dc09faff777..5850a4d9129fb398652cff6a33ea446dca70ba52 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -1,6 +1,7 @@ package net.sourceforge.opencamera; import net.sourceforge.opencamera.cameracontroller.CameraController; +import net.sourceforge.opencamera.cameracontroller.CameraControllerManager; import net.sourceforge.opencamera.cameracontroller.CameraControllerManager2; import net.sourceforge.opencamera.preview.Preview; import net.sourceforge.opencamera.preview.VideoProfile; @@ -78,6 +79,7 @@ import android.view.TextureView; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.WindowManager; import android.widget.EditText; import android.widget.ImageButton; @@ -94,6 +96,8 @@ public class MainActivity extends Activity { private static int activity_count = 0; + private boolean app_is_paused = true; + private SensorManager mSensorManager; private Sensor mSensorAccelerometer; @@ -132,6 +136,22 @@ public class MainActivity extends Activity { //private boolean ui_placement_right = true; + private boolean want_no_limits; // whether we want to run with FLAG_LAYOUT_NO_LIMITS + private boolean set_window_insets_listener; // whether we've enabled a setOnApplyWindowInsetsListener() + private int navigation_gap; + public static volatile boolean test_preview_want_no_limits; // test flag, if set to true then instead use test_preview_want_no_limits_value; needs to be static, as it needs to be set before activity is created to take effect + public static volatile boolean test_preview_want_no_limits_value; + + // whether this is a multi-camera device (note, this isn't simply having more than 1 camera, but also having more than one with the same facing) + // note that in most cases, code should check the MultiCamButtonPreferenceKey preference as well as the is_multi_cam flag, + // this can be done via isMultiCamEnabled(). + private boolean is_multi_cam; + // These lists are lists of camera IDs with the same "facing" (front, back or external). + // Only initialised if is_multi_cam==true. + private List back_camera_ids; + private List front_camera_ids; + private List other_camera_ids; + private final ToastBoxer switch_video_toast = new ToastBoxer(); private final ToastBoxer screen_locked_toast = new ToastBoxer(); private final ToastBoxer stamp_toast = new ToastBoxer(); @@ -140,6 +160,7 @@ public class MainActivity extends Activity { private final ToastBoxer exposure_lock_toast = new ToastBoxer(); private final ToastBoxer audio_control_toast = new ToastBoxer(); private boolean block_startup_toast = false; // used when returning from Settings/Popup - if we're displaying a toast anyway, don't want to display the info toast too + private String push_info_toast_text; // can be used to "push" extra text to the info text for showPhotoVideoToast() // application shortcuts: static private final String ACTION_SHORTCUT_CAMERA = "net.sourceforge.opencamera.SHORTCUT_CAMERA"; @@ -202,10 +223,14 @@ public class MainActivity extends Activity { if( MyDebug.LOG ) Log.d(TAG, "is_test: " + is_test); } - if( getIntent() != null && getIntent().getExtras() != null ) { + /*if( getIntent() != null && getIntent().getExtras() != null ) { // whether called from Take Photo widget if( MyDebug.LOG ) Log.d(TAG, "take_photo?: " + getIntent().getExtras().getBoolean(TakePhoto.TAKE_PHOTO)); + }*/ + if( MyDebug.LOG ) { + // whether called from Take Photo widget + Log.d(TAG, "take_photo?: " + TakePhoto.TAKE_PHOTO); } if( getIntent() != null && getIntent().getAction() != null ) { // invoked via the manifest shortcut? @@ -254,12 +279,6 @@ public class MainActivity extends Activity { // determine whether we support Camera2 API initCamera2Support(); - if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ) { - // no point having talkback care about this - and (hopefully) avoid Google Play pre-launch accessibility warnings - View container = findViewById(R.id.hide_container); - container.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - } - // set up window flags for normal operation setWindowFlagsForCamera(); if( MyDebug.LOG ) @@ -296,16 +315,68 @@ public class MainActivity extends Activity { Log.d(TAG, "onCreate: time after creating magnetic sensor: " + (System.currentTimeMillis() - debug_time)); // clear any seek bars (just in case??) - mainUI.clearSeekBar(); + mainUI.closeExposureUI(); // set up the camera and its preview preview = new Preview(applicationInterface, ((ViewGroup) this.findViewById(R.id.preview))); if( MyDebug.LOG ) Log.d(TAG, "onCreate: time after creating preview: " + (System.currentTimeMillis() - debug_time)); + // Setup multi-camera buttons (must be done after creating preview so we know which Camera API is being used, + // and before initialising on-screen visibility). + // We only allow the separate icon for switching cameras if: + // - there are at least 2 types of "facing" camera, and + // - there are at least 2 cameras with the same "facing". + // If there are multiple cameras but all with different "facing", then the switch camera + // icon is used to iterate over all cameras. + // If there are more than two cameras, but all cameras have the same "facing, we still stick + // with using the switch camera icon to iterate over all cameras. + int n_cameras = preview.getCameraControllerManager().getNumberOfCameras(); + if( n_cameras > 2 ) { + this.back_camera_ids = new ArrayList<>(); + this.front_camera_ids = new ArrayList<>(); + this.other_camera_ids = new ArrayList<>(); + for(int i=0;i= 2 || front_camera_ids.size() >= 2 || other_camera_ids.size() >= 2; + int n_facing = 0; + if( back_camera_ids.size() > 0 ) + n_facing++; + if( front_camera_ids.size() > 0 ) + n_facing++; + if( other_camera_ids.size() > 0 ) + n_facing++; + this.is_multi_cam = multi_same_facing && n_facing >= 2; + //this.is_multi_cam = false; // test + if( MyDebug.LOG ) { + Log.d(TAG, "multi_same_facing: " + multi_same_facing); + Log.d(TAG, "n_facing: " + n_facing); + Log.d(TAG, "is_multi_cam: " + is_multi_cam); + } + + if( !is_multi_cam ) { + this.back_camera_ids = null; + this.front_camera_ids = null; + this.other_camera_ids = null; + } + } + // initialise on-screen button visibility View switchCameraButton = findViewById(R.id.switch_camera); - switchCameraButton.setVisibility(preview.getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE); + switchCameraButton.setVisibility(n_cameras > 1 ? View.VISIBLE : View.GONE); + // switchMultiCameraButton visibility updated below in mainUI.updateOnScreenIcons(), as it also depends on user preference View speechRecognizerButton = findViewById(R.id.audio_control); speechRecognizerButton.setVisibility(View.GONE); // disabled by default, until the speech recognizer is created if( MyDebug.LOG ) @@ -347,6 +418,10 @@ public class MainActivity extends Activity { takePhotoButton.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { + if( !allowLongPress() ) { + // return false, so a regular click will still be triggered when the user releases the touch + return false; + } return longClickedTakePhoto(); } }); @@ -375,6 +450,10 @@ public class MainActivity extends Activity { galleryButton.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { + if( !allowLongPress() ) { + // return false, so a regular click will still be triggered when the user releases the touch + return false; + } //preview.showToast(null, "Long click"); longClickedGallery(); return true; @@ -388,8 +467,38 @@ public class MainActivity extends Activity { if( MyDebug.LOG ) Log.d(TAG, "onCreate: time after creating gesture detector: " + (System.currentTimeMillis() - debug_time)); - // set up listener to handle immersive mode options View decorView = getWindow().getDecorView(); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { + // set a window insets listener to find the navigation_gap + if( MyDebug.LOG ) + Log.d(TAG, "set a window insets listener"); + this.set_window_insets_listener = true; + decorView.getRootView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + if( MyDebug.LOG ) + Log.d(TAG, "inset: " + insets.getSystemWindowInsetRight()); + if( navigation_gap == 0 ) { + navigation_gap = insets.getSystemWindowInsetRight(); + if( MyDebug.LOG ) + Log.d(TAG, "navigation_gap is " + navigation_gap); + // Sometimes when this callback is called, the navigation_gap may still be 0 even if + // the device doesn't have physical navigation buttons - we need to wait + // until we have found a non-zero value before switching to no limits. + // On devices with physical navigation bar, navigation_gap should remain 0 + // (and there's no point setting FLAG_LAYOUT_NO_LIMITS) + if( want_no_limits && navigation_gap != 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS"); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + } + return getWindow().getDecorView().getRootView().onApplyWindowInsets(insets); + } + }); + } + + // set up listener to handle immersive mode options decorView.setOnSystemUiVisibilityChangeListener (new View.OnSystemUiVisibilityChangeListener() { @Override @@ -400,13 +509,19 @@ public class MainActivity extends Activity { return; if( MyDebug.LOG ) Log.d(TAG, "onSystemUiVisibilityChange: " + visibility); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_low_profile"); + boolean hide_ui = immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything"); + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { if( MyDebug.LOG ) Log.d(TAG, "system bars now visible"); // The system bars are visible. Make any desired // adjustments to your UI, such as showing the action bar or // other navigational controls. - mainUI.setImmersiveMode(false); + if( hide_ui ) + mainUI.setImmersiveMode(false); setImmersiveTimer(); } else { @@ -415,7 +530,8 @@ public class MainActivity extends Activity { // The system bars are NOT visible. Make any desired // adjustments to your UI, such as hiding the action bar or // other navigational controls. - mainUI.setImmersiveMode(true); + if( hide_ui ) + mainUI.setImmersiveMode(true); } } }); @@ -475,7 +591,7 @@ public class MainActivity extends Activity { // E.g., we have a "What's New" for 1.44 (64), but then push out a quick fix for 1.44.1 (65). We don't want to // show the dialog again to people who already received 1.44 (64), but we still want to show the dialog to people // upgrading from earlier versions. - int whats_new_version = 74; // 1.47.3 + int whats_new_version = 75; // 1.48 whats_new_version = Math.min(whats_new_version, version_code); // whats_new_version should always be <= version_code, but just in case! if( MyDebug.LOG ) { Log.d(TAG, "whats_new_version: " + whats_new_version); @@ -562,6 +678,72 @@ public class MainActivity extends Activity { Log.d(TAG, "onCreate: total time for Activity startup: " + (System.currentTimeMillis() - debug_time)); } + public int getNavigationGap() { + return want_no_limits ? navigation_gap : 0; + } + + /** Whether this is a multi camera device, and the user preference is set to enable the multi-camera button. + */ + public boolean isMultiCamEnabled() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + return is_multi_cam && sharedPreferences.getBoolean(PreferenceKeys.MultiCamButtonPreferenceKey, true); + } + + /** Whether this is a multi camera device, whether or not the user preference is set to enable + * the multi-camera button. + */ + public boolean isMultiCam() { + return is_multi_cam; + } + + /* Returns the camera Id in use by the preview - or the one we requested, if the camera failed + * to open. + * Needed as Preview.getCameraId() returns 0 if camera_controller==null, but if the camera + * fails to open, we want the switch camera icons to still work as expected! + */ + private int getActualCameraId() { + if( preview.getCameraController() == null ) + return applicationInterface.getCameraIdPref(); + else + return preview.getCameraId(); + } + + /** Whether the icon switch_multi_camera should be displayed. This is if the following are all + * true: + * - The device is a multi camera device (MainActivity.is_multi_cam==true). + * - The user preference for using the separate icons is enabled + * (PreferenceKeys.MultiCamButtonPreferenceKey). + * - For the current camera ID, there is only one camera with the same front/back/external + * "facing" (e.g., imagine a device with two back cameras, but only one front camera). + */ + public boolean showSwitchMultiCamIcon() { + if( isMultiCamEnabled() ) { + int cameraId = getActualCameraId(); + switch( preview.getCameraControllerManager().getFacing(cameraId) ) { + case FACING_BACK: + if( back_camera_ids.size() > 0 ) + return true; + break; + case FACING_FRONT: + if( front_camera_ids.size() > 0 ) + return true; + break; + default: + if( other_camera_ids.size() > 0 ) + return true; + break; + } + } + return false; + } + + /** Whether user preference is set to allow long press actions. + */ + private boolean allowLongPress() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + return sharedPreferences.getBoolean(PreferenceKeys.AllowLongPressPreferenceKey, true); + } + /* This method sets the preference defaults which are set specific for a particular device. * This method should be called when Open Camera is run for the very first time after installation, * or when the user has requested to "Reset settings". @@ -788,6 +970,16 @@ public class MainActivity extends Activity { } } + @Override + protected void onStop() { + if( MyDebug.LOG ) + Log.d(TAG, "onStop"); + super.onStop(); + + // we stop location listening in onPause, but done here again just to be certain! + applicationInterface.getLocationSupplier().freeLocationListeners(); + } + @Override protected void onDestroy() { if( MyDebug.LOG ) { @@ -840,6 +1032,9 @@ public class MainActivity extends Activity { textToSpeech = null; } + // we stop location listening in onPause, but done here again just to be certain! + applicationInterface.getLocationSupplier().freeLocationListeners(); + super.onDestroy(); if( MyDebug.LOG ) Log.d(TAG, "onDestroy done"); @@ -865,7 +1060,8 @@ public class MainActivity extends Activity { if (MyDebug.LOG) Log.d(TAG, "getOnlineHelpUrl: " + append); // if we change this, remember that any page linked to must abide by Google Play developer policies! - return "https://opencamera.sourceforge.io/"+ append; + //return "https://opencamera.sourceforge.io/" + append; + return "https://opencamera.org.uk/" + append; } void launchOnlineHelp() { @@ -880,8 +1076,8 @@ public class MainActivity extends Activity { if( MyDebug.LOG ) Log.d(TAG, "launchOnlinePrivacyPolicy"); // if we change this, remember that any page linked to must abide by Google Play developer policies! - //Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://opencamera.sourceforge.io/index.html#privacy")); - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://opencamera.sourceforge.io/privacy_oc.html")); + //Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("index.html#privacy"))); + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("privacy_oc.html"))); startActivity(browserIntent); } @@ -932,16 +1128,30 @@ public class MainActivity extends Activity { public boolean onKeyDown(int keyCode, KeyEvent event) { if( MyDebug.LOG ) Log.d(TAG, "onKeyDown: " + keyCode); - boolean handled = mainUI.onKeyDown(keyCode, event); - if( handled ) - return true; + if( camera_in_background ) { + // don't allow keys such as volume keys for taking photo when camera in background! + if( MyDebug.LOG ) + Log.d(TAG, "camera is in background"); + } + else { + boolean handled = mainUI.onKeyDown(keyCode, event); + if( handled ) + return true; + } return super.onKeyDown(keyCode, event); } public boolean onKeyUp(int keyCode, KeyEvent event) { if( MyDebug.LOG ) Log.d(TAG, "onKeyUp: " + keyCode); - mainUI.onKeyUp(keyCode, event); + if( camera_in_background ) { + // don't allow keys such as volume keys for taking photo when camera in background! + if( MyDebug.LOG ) + Log.d(TAG, "camera is in background"); + } + else { + mainUI.onKeyUp(keyCode, event); + } return super.onKeyUp(keyCode, event); } @@ -1001,6 +1211,7 @@ public class MainActivity extends Activity { debug_time = System.currentTimeMillis(); } super.onResume(); + this.app_is_paused = false; // must be set before initLocation() at least cancelImageSavingNotification(); @@ -1028,10 +1239,40 @@ public class MainActivity extends Activity { updateGalleryIcon(); // update in case images deleted whilst idle - applicationInterface.reset(); // should be called before opening the camera in preview.onResume() + applicationInterface.reset(false); // should be called before opening the camera in preview.onResume() preview.onResume(); + { + // show a toast for the camera if it's not the first for front of back facing (otherwise on multi-front/back camera + // devices, it's easy to forget if set to a different camera) + // but we only show this when resuming, not every time the camera opens + int cameraId = applicationInterface.getCameraIdPref(); + if( cameraId > 0 ) { + CameraControllerManager camera_controller_manager = preview.getCameraControllerManager(); + CameraController.Facing front_facing = camera_controller_manager.getFacing(cameraId); + if( MyDebug.LOG ) + Log.d(TAG, "front_facing: " + front_facing); + if( camera_controller_manager.getNumberOfCameras() > 2 ) { + boolean camera_is_default = true; + for(int i=0;i 0 ) + cameraId = front_camera_ids.get(0); + else if( other_camera_ids.size() > 0 ) + cameraId = other_camera_ids.get(0); + break; + case FACING_FRONT: + if( other_camera_ids.size() > 0 ) + cameraId = other_camera_ids.get(0); + else if( back_camera_ids.size() > 0 ) + cameraId = back_camera_ids.get(0); + break; + default: + if( back_camera_ids.size() > 0 ) + cameraId = back_camera_ids.get(0); + else if( front_camera_ids.size() > 0 ) + cameraId = front_camera_ids.get(0); + break; + } + } + else { + int n_cameras = preview.getCameraControllerManager().getNumberOfCameras(); + cameraId = (cameraId+1) % n_cameras; + } } if( MyDebug.LOG ) Log.d(TAG, "next cameraId: " + cameraId); return cameraId; } + /* Returns the cameraId that the "Switch multi camera" button will switch to. + * Should only be called if isMultiCamEnabled() returns true. + */ + public int getNextMultiCameraId() { + if( MyDebug.LOG ) + Log.d(TAG, "getNextMultiCameraId"); + if( !isMultiCamEnabled() ) { + Log.e(TAG, "getNextMultiCameraId() called but not in multi-cam mode"); + throw new RuntimeException("getNextMultiCameraId() called but not in multi-cam mode"); + } + List camera_set; + // don't use preview.getCameraController(), as it may be null if user quickly switches between cameras + int currCameraId = getActualCameraId(); + switch( preview.getCameraControllerManager().getFacing(currCameraId) ) { + case FACING_BACK: + camera_set = back_camera_ids; + break; + case FACING_FRONT: + camera_set = front_camera_ids; + break; + default: + camera_set = other_camera_ids; + break; + } + int cameraId; + int indx = camera_set.indexOf(currCameraId); + if( indx == -1 ) { + Log.e(TAG, "camera id not in current camera set"); + // this shouldn't happen, but if it does, revert to the first camera id in the set + cameraId = camera_set.get(0); + } + else { + indx = (indx+1) % camera_set.size(); + cameraId = camera_set.get(indx); + } + if( MyDebug.LOG ) + Log.d(TAG, "next multi cameraId: " + cameraId); + return cameraId; + } + + private void pushCameraIdToast(int cameraId) { + if( MyDebug.LOG ) + Log.d(TAG, "pushCameraIdToast: " + cameraId); + if( preview.getCameraControllerManager().getNumberOfCameras() > 2 ) { + // telling the user which camera is pointless for only two cameras, but on devices that now + // expose many cameras it can be confusing, so show a toast to at least display the id + String description = preview.getCameraControllerManager().getDescription(this, cameraId); + if( description != null ) { + String toast_string = description + ": "; + toast_string += getResources().getString(R.string.camera_id) + " " + cameraId; + //preview.showToast(null, toast_string); + this.push_info_toast_text = toast_string; + } + } + } + + private void userSwitchToCamera(int cameraId) { + if( MyDebug.LOG ) + Log.d(TAG, "userSwitchToCamera: " + cameraId); + View switchCameraButton = findViewById(R.id.switch_camera); + View switchMultiCameraButton = findViewById(R.id.switch_multi_camera); + // prevent slowdown if user repeatedly clicks: + switchCameraButton.setEnabled(false); + switchMultiCameraButton.setEnabled(false); + applicationInterface.reset(true); + this.preview.setCamera(cameraId); + switchCameraButton.setEnabled(true); + switchMultiCameraButton.setEnabled(true); + // no need to call mainUI.setSwitchCameraContentDescription - this will be called from Preview.cameraSetup when the + // new camera is opened + } + /** * Selects the next camera on the phone - in practice, switches between * front and back cameras - * @param view */ public void clickedSwitchCamera(View view) { if( MyDebug.LOG ) @@ -1422,28 +1766,45 @@ public class MainActivity extends Activity { this.closePopup(); if( this.preview.canSwitchCamera() ) { int cameraId = getNextCameraId(); - if( preview.getCameraControllerManager().getNumberOfCameras() > 2 ) { - // telling the user which camera is pointless for only two cameras, but on devices that now - // expose many cameras it can be confusing, so show a toast to at least display the id - String toast_string = getResources().getString( - preview.getCameraControllerManager().isFrontFacing(cameraId) ? R.string.front_camera : R.string.back_camera ) + - " : " + getResources().getString(R.string.camera_id) + " " + cameraId; - preview.showToast(null, toast_string); + if( !isMultiCamEnabled() ) { + pushCameraIdToast(cameraId); + } + else { + // In multi-cam mode, no need to show the toast when just switching between front and back cameras. + // But it is useful to clear an active fake toast, otherwise have issue if the user uses + // clickedSwitchMultiCamera() (which displays a fake toast for the camera via the info toast), then + // immediately uses clickedSwitchCamera() - the toast for the wrong camera will still be lingering + // until it expires, which looks a bit strange. + // (If using non-fake toasts, this isn't an issue, at least on Android 10+, as now toasts seem to + // disappear when the user touches the screen anyway.) + preview.clearActiveFakeToast(); } + userSwitchToCamera(cameraId); + } + } - View switchCameraButton = findViewById(R.id.switch_camera); - switchCameraButton.setEnabled(false); // prevent slowdown if user repeatedly clicks - applicationInterface.reset(); - this.preview.setCamera(cameraId); - switchCameraButton.setEnabled(true); - // no need to call mainUI.setSwitchCameraContentDescription - this will be called from PreviewcameraSetup when the - // new camera is opened + public void clickedSwitchMultiCamera(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedSwitchMultiCamera"); + if( !isMultiCamEnabled() ) { + Log.e(TAG, "switch multi camera icon shouldn't have been visible"); + return; + } + if( preview.isOpeningCamera() ) { + if( MyDebug.LOG ) + Log.d(TAG, "already opening camera in background thread"); + return; + } + this.closePopup(); + if( this.preview.canSwitchCamera() ) { + int cameraId = getNextMultiCameraId(); + pushCameraIdToast(cameraId); + userSwitchToCamera(cameraId); } } /** * Toggles Photo/Video mode - * @param view */ public void clickedSwitchVideo(View view) { if( MyDebug.LOG ) @@ -1459,14 +1820,14 @@ public class MainActivity extends Activity { View switchVideoButton = findViewById(R.id.switch_video); switchVideoButton.setEnabled(false); // prevent slowdown if user repeatedly clicks - applicationInterface.reset(); + applicationInterface.reset(false); this.preview.switchVideo(false, true); switchVideoButton.setEnabled(true); mainUI.setTakePhotoIcon(); mainUI.setPopupIcon(); // needed as turning to video mode or back can turn flash mode off or back on - // ensure icons invisible if they're affected by being in video mode or not + // ensure icons invisible if they're affected by being in video mode or not (e.g., on-screen RAW icon) // (if enabling them, we'll make the icon visible later on) checkDisableGUIIcons(); @@ -1534,7 +1895,7 @@ public class MainActivity extends Activity { private static final String TAG = "PreferencesListener"; private boolean any_significant_change; // whether any changes that require updateForSettings have been made since startListening() - private boolean any_change; // whether any changes that require updateForSettings have been made since startListening() + private boolean any_change; // whether any changes have been made since startListening() void startListening() { if( MyDebug.LOG ) @@ -1564,23 +1925,35 @@ public class MainActivity extends Activity { switch( key ) { // we whitelist preferences where we're sure that we don't need to call updateForSettings() if they've changed + //case "preference_face_detection": // need to update camera controller case "preference_timer": case "preference_burst_mode": case "preference_burst_interval": - //case "preference_ghost_image": // don't whitelist this, as may need to reload ghost image (at fullscreen resolution) if "last" is enabled case "preference_touch_capture": case "preference_pause_preview": case "preference_shutter_sound": case "preference_timer_beep": case "preference_timer_speak": case "preference_volume_keys": + //case "preference_audio_control": // need to update the UI case "preference_audio_noise_control_sensitivity": + //case "preference_enable_remote": // handled below + //case "preference_remote_type": + //case "preference_remote_device_name": // handled below + //case "preference_remote_disconnect_screen_dim": + //case "preference_water_type": // handled below + case "preference_lock_orientation": + //case "preference_save_location": // we could probably whitelist this, but accessed it a lot of places... case "preference_using_saf": case "preference_save_photo_prefix": case "preference_save_video_prefix": case "preference_save_zulu_time": case "preference_show_when_locked": case "preference_startup_focus": + //case "preference_preview_size": // need to update preview + //case "preference_ghost_image": // don't whitelist this, as may need to reload ghost image (at fullscreen resolution) if "last" is enabled + case "ghost_image_alpha": + case "preference_focus_assist": case "preference_show_zoom": case "preference_show_angle": case "preference_show_angle_line": @@ -1592,30 +1965,93 @@ public class MainActivity extends Activity { case "preference_show_time": case "preference_free_memory": case "preference_show_iso": + case "preference_histogram": + case "preference_zebra_stripes": + case "preference_zebra_stripes_foreground_color": + case "preference_zebra_stripes_background_color": + case "preference_focus_peaking": + case "preference_focus_peaking_color": + case "preference_show_video_max_amp": case "preference_grid": case "preference_crop_guide": - case "preference_show_toasts": case "preference_thumbnail_animation": case "preference_take_photo_border": + //case "preference_rotate_preview": // need to update the Preview + //case "preference_ui_placement": // need to update the UI + //case "preference_immersive_mode": // probably could whitelist? + //case "preference_show_face_detection": // need to update the UI + //case "preference_show_cycle_flash": // need to update the UI + //case "preference_show_auto_level": // need to update the UI + //case "preference_show_stamp": // need to update the UI + //case "preference_show_textstamp": // need to update the UI + //case "preference_show_store_location": // need to update the UI + //case "preference_show_cycle_raw": // need to update the UI + //case "preference_show_white_balance_lock": // need to update the UI + //case "preference_show_exposure_lock": // need to update the UI + //case "preference_show_zoom_controls": // need to update the UI + //case "preference_show_zoom_slider_controls": // need to update the UI + //case "preference_show_take_photo": // need to update the UI + case "preference_show_toasts": + case "preference_show_whats_new": + //case "preference_multi_cam_button": // need to update the UI case "preference_keep_display_on": case "preference_max_brightness": + //case "preference_resolution": // need to set up camera controller and preview + //case "preference_quality": // need to set up camera controller + //case "preference_image_format": // need to set up camera controller (as it can affect the image quality that we set) + //case "preference_raw": // need to update as it affects how we set up camera controller + //case "preference_raw_expo_bracketing": // as above + //case "preference_raw_focus_bracketing": // as above + //case "preference_nr_save": // we could probably whitelist this, but have not done so in case in future we allow RAW to be saved for the base image //case "preference_hdr_save_expo": // we need to update if this is changed, as it affects whether we request RAW or not in HDR mode when RAW is enabled + case "preference_hdr_contrast_enhancement": + //case "preference_expo_bracketing_n_images": // need to set up camera controller + //case "preference_expo_bracketing_stops": // need to set up camera controller + case "preference_panorama_crop": + //case "preference_panorama_save": // we could probably whitelist this, but have not done so in case in future we allow RAW to be saved for the base images case "preference_front_camera_mirror": + case "preference_exif_artist": + case "preference_exif_copyright": case "preference_stamp": case "preference_stamp_dateformat": case "preference_stamp_timeformat": case "preference_stamp_gpsformat": + case "preference_stamp_geo_address": + case "preference_units_distance": case "preference_textstamp": case "preference_stamp_fontsize": case "preference_stamp_font_color": case "preference_stamp_style": + //case "preference_camera2_fake_flash": // need to update camera controller + //case "preference_camera2_fast_burst": // could probably whitelist? + //case "preference_camera2_photo_video_recording": // need to update camera controller case "preference_background_photo_saving": + //case "preference_video_quality": // need to update camera controller and preview + //case "preference_video_stabilization": // need to update camera controller + //case "preference_video_output_format": // could probably whitelist, but safest to restart camera + //case "preference_video_log": // need to update camera controller + //case "preference_video_profile_gamma": // as above + //case "preference_video_max_duration": // could probably whitelist, but safest to restart camera + //case "preference_video_restart": // could probably whitelist, but safest to restart camera + //case "preference_video_max_filesize": // could probably whitelist, but safest to restart camera + //case "preference_video_restart_max_filesize": // could probably whitelist, but safest to restart camera case "preference_record_audio": case "preference_record_audio_src": case "preference_record_audio_channels": case "preference_lock_video": case "preference_video_subtitle": + //case "preference_video_bitrate": // could probably whitelist, but safest to restart camera + //case "preference_video_fps": // could probably whitelist, but safest to restart camera + //case "preference_force_video_4k": // could probably whitelist, but safest to restart camera + case "preference_video_low_power_check": + case "preference_video_flash": + //case "preference_location": // need to enable/disable gps listeners etc + //case "preference_gps_direction": // need to update listeners case "preference_require_location": + //case "preference_antibanding": // need to set up camera controller + //case "preference_edge_mode": // need to set up camera controller + //case "preference_noise_reduction_mode": // need to set up camera controller + //case "preference_camera_api": // no point whitelisting as we restart anyway if( MyDebug.LOG ) Log.d(TAG, "this change doesn't require update"); break; @@ -1664,6 +2100,7 @@ public class MainActivity extends Activity { bundle.putInt("nCameras", preview.getCameraControllerManager().getNumberOfCameras()); bundle.putString("camera_api", this.preview.getCameraAPI()); bundle.putBoolean("using_android_l", this.preview.usingCamera2API()); + bundle.putString("photo_mode_string", getPhotoModeString(applicationInterface.getPhotoMode(), true)); bundle.putBoolean("supports_auto_stabilise", this.supports_auto_stabilise); bundle.putBoolean("supports_flash", this.preview.supportsFlash()); bundle.putBoolean("supports_force_video_4k", this.supports_force_video_4k); @@ -1674,6 +2111,7 @@ public class MainActivity extends Activity { bundle.putBoolean("supports_hdr", this.supportsHDR()); bundle.putBoolean("supports_nr", this.supportsNoiseReduction()); bundle.putBoolean("supports_panorama", this.supportsPanorama()); + bundle.putBoolean("has_gyro_sensors", applicationInterface.getGyroSensor().hasSensors()); bundle.putBoolean("supports_expo_bracketing", this.supportsExpoBracketing()); bundle.putBoolean("supports_preview_bitmaps", this.supportsPreviewBitmaps()); bundle.putInt("max_expo_bracketing_n_images", this.maxExpoBracketingNImages()); @@ -1691,7 +2129,11 @@ public class MainActivity extends Activity { bundle.putBoolean("supports_white_balance_temperature", this.preview.supportsWhiteBalanceTemperature()); bundle.putInt("white_balance_temperature_min", this.preview.getMinimumWhiteBalanceTemperature()); bundle.putInt("white_balance_temperature_max", this.preview.getMaximumWhiteBalanceTemperature()); + bundle.putBoolean("is_multi_cam", this.is_multi_cam); + bundle.putBoolean("supports_optical_stabilization", this.preview.supportsOpticalStabilization()); + bundle.putBoolean("optical_stabilization_enabled", this.preview.getOpticalStabilization()); bundle.putBoolean("supports_video_stabilization", this.preview.supportsVideoStabilization()); + bundle.putBoolean("video_stabilization_enabled", this.preview.getVideoStabilization()); bundle.putBoolean("can_disable_shutter_sound", this.preview.canDisableShutterSound()); bundle.putInt("tonemap_max_curve_points", this.preview.getTonemapMaxCurvePoints()); bundle.putBoolean("supports_tonemap_curve", this.preview.supportsTonemapCurve()); @@ -1902,6 +2344,9 @@ public class MainActivity extends Activity { /** Must be called when an settings (as stored in SharedPreferences) are made, so we can update the * camera, and make any other necessary changes. + * @param toast_message If non-null, display this toast instead of the usual camera "startup" toast + * that's shown in showPhotoVideoToast(). If non-null but an empty string, then + * this means no toast is shown at all. * @param keep_popup If false, the popup will be closed and destroyed. Set to true if you're sure * that the changed setting isn't one that requires the PopupView to be recreated */ @@ -1970,6 +2415,20 @@ public class MainActivity extends Activity { } } } + + if( !need_reopen ) { + CameraController.TonemapProfile old_tonemap_profile = preview.getCameraController().getTonemapProfile(); + if( old_tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF ) { + CameraController.TonemapProfile new_tonemap_profile = applicationInterface.getVideoTonemapProfile(); + if( new_tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && new_tonemap_profile != old_tonemap_profile ) { + // needed for Galaxy S10e when changing from TONEMAP_MODE_CONTRAST_CURVE to TONEMAP_MODE_PRESET_CURVE, + // otherwise the contrast curve remains active! + if( MyDebug.LOG ) + Log.d(TAG, "switching between tonemap profiles"); + need_reopen = true; + } + } + } } if( MyDebug.LOG ) { Log.d(TAG, "updateForSettings: time after check need_reopen: " + (System.currentTimeMillis() - debug_time)); @@ -1991,8 +2450,14 @@ public class MainActivity extends Activity { } speechControl.initSpeechRecognizer(); // in case we've enabled or disabled speech recognizer - initLocation(); // in case we've enabled or disabled GPS - initGyroSensors(); // in case we've entered or left panoram + + // we no longer call initLocation() here (for having enabled or disabled geotagging), as that's + // done in setWindowFlagsForCamera() - important not to call it here as well, otherwise if + // permission wasn't granted, we'll ask for permission twice in a row (on Android 9 or earlier + // at least) + //initLocation(); // in case we've enabled or disabled GPS + + initGyroSensors(); // in case we've entered or left panorama if( MyDebug.LOG ) { Log.d(TAG, "updateForSettings: time after init speech and location: " + (System.currentTimeMillis() - debug_time)); } @@ -2037,45 +2502,67 @@ public class MainActivity extends Activity { } } - private void checkDisableGUIIcons() { + /** Disables the optional on-screen icons if either user doesn't want to enable them, or not + * supported). Note that displaying icons is done via MainUI.showGUI. + * @return Whether an icon's visibility was changed. + */ + private boolean checkDisableGUIIcons() { if( MyDebug.LOG ) Log.d(TAG, "checkDisableGUIIcons"); + boolean changed = false; if( !mainUI.showExposureLockIcon() ) { View button = findViewById(R.id.exposure_lock); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showWhiteBalanceLockIcon() ) { View button = findViewById(R.id.white_balance_lock); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showCycleRawIcon() ) { View button = findViewById(R.id.cycle_raw); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showStoreLocationIcon() ) { View button = findViewById(R.id.store_location); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showTextStampIcon() ) { View button = findViewById(R.id.text_stamp); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showStampIcon() ) { View button = findViewById(R.id.stamp); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showAutoLevelIcon() ) { View button = findViewById(R.id.auto_level); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showCycleFlashIcon() ) { View button = findViewById(R.id.cycle_flash); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } if( !mainUI.showFaceDetectionIcon() ) { View button = findViewById(R.id.face_detection); + changed = changed || (button.getVisibility() != View.GONE); button.setVisibility(View.GONE); } + if( !showSwitchMultiCamIcon() ) { + // also handle the multi-cam icon here, as this can change when switching between front/back cameras + // (e.g., if say a device only has multiple back cameras) + View button = findViewById(R.id.switch_multi_camera); + changed = changed || (button.getVisibility() != View.GONE); + button.setVisibility(View.GONE); + } + return changed; } public MyPreferenceFragment getPreferenceFragment() { @@ -2097,7 +2584,7 @@ public class MainActivity extends Activity { preferencesListener.stopListening(); // Update the cached settings in DrawPreview - // Note that some GUI related settings won't trigger preferencesListener.anyChanges(), so + // Note that some GUI related settings won't trigger preferencesListener.anyChange(), so // we always call this. Perhaps we could add more classifications to PreferencesListener // to mark settings that need us to update DrawPreview but not call updateForSettings(). // However, DrawPreview.updateSettings() should be a quick function (the main point is @@ -2105,7 +2592,6 @@ public class MainActivity extends Activity { applicationInterface.getDrawPreview().updateSettings(); if( preferencesListener.anyChange() ) { - // in case face detection etc enabled/disabled in settings: mainUI.updateOnScreenIcons(); } @@ -2154,7 +2640,7 @@ public class MainActivity extends Activity { if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_low_profile"); - if( immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything") ) + if( immersive_mode.equals("immersive_mode_navigation") || immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything") ) return true; } return false; @@ -2237,7 +2723,7 @@ public class MainActivity extends Activity { // done here rather than onCreate, so that changing it in preferences takes effect without restarting app SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); final WindowManager.LayoutParams layout = getWindow().getAttributes(); - if( force_max || sharedPreferences.getBoolean(PreferenceKeys.getMaxBrightnessPreferenceKey(), true) ) { + if( force_max || sharedPreferences.getBoolean(PreferenceKeys.MaxBrightnessPreferenceKey, true) ) { layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL; } else { @@ -2316,7 +2802,7 @@ public class MainActivity extends Activity { // keep screen active - see http://stackoverflow.com/questions/2131948/force-screen-on - if( sharedPreferences.getBoolean(PreferenceKeys.getKeepDisplayOnPreferenceKey(), true) ) { + if( sharedPreferences.getBoolean(PreferenceKeys.KeepDisplayOnPreferenceKey, true) ) { if( MyDebug.LOG ) Log.d(TAG, "do keep screen on"); this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -2326,7 +2812,7 @@ public class MainActivity extends Activity { Log.d(TAG, "don't keep screen on"); this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } - if( sharedPreferences.getBoolean(PreferenceKeys.getShowWhenLockedPreferenceKey(), true) ) { + if( sharedPreferences.getBoolean(PreferenceKeys.ShowWhenLockedPreferenceKey, true) ) { if( MyDebug.LOG ) Log.d(TAG, "do show when locked"); // keep Open Camera on top of screen-lock (will still need to unlock when going to gallery or settings) @@ -2338,12 +2824,26 @@ public class MainActivity extends Activity { showWhenLocked(false); } + if( want_no_limits && navigation_gap != 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS"); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + setBrightnessForCamera(false); initImmersiveMode(); camera_in_background = false; magneticSensor.clearDialog(); // if the magnetic accuracy was opened, it must have been closed now + if( !app_is_paused ) { + // Needs to be called after camera_in_background is set to false. + // Note that the app_is_paused guard is in some sense unnecessary, as initLocation tests for that too, + // but useful for error tracking - ideally we want to make sure that initLocation is never called when + // app is paused. It can happen here because setWindowFlagsForCamera() is called from + // onCreate() + initLocation(); + } } private void setWindowFlagsForSettings() { @@ -2366,6 +2866,11 @@ public class MainActivity extends Activity { // revert to standard screen blank behaviour getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if( want_no_limits && navigation_gap != 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS"); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } if( set_lock_protect ) { // settings should still be protected by screen lock showWhenLocked(false); @@ -2379,6 +2884,9 @@ public class MainActivity extends Activity { setImmersiveMode(false); camera_in_background = true; + + // we disable location listening when showing settings or a dialog etc - saves battery life, also better for privacy + applicationInterface.getLocationSupplier().freeLocationListeners(); } private void showWhenLocked(boolean show) { @@ -2511,10 +3019,12 @@ public class MainActivity extends Activity { } // align dimensions if( display_size.x < display_size.y ) { + //noinspection SuspiciousNameCombination display_size.set(display_size.y, display_size.x); } if( bitmap_width < bitmap_height ) { int dummy = bitmap_width; + //noinspection SuspiciousNameCombination bitmap_width = bitmap_height; bitmap_height = dummy; } @@ -2570,6 +3080,8 @@ public class MainActivity extends Activity { } } if( thumbnail != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "thumbnail orientation is " + media.orientation); if( media.orientation != 0 ) { if( MyDebug.LOG ) Log.d(TAG, "thumbnail size is " + thumbnail.getWidth() + " x " + thumbnail.getHeight()); @@ -2719,7 +3231,7 @@ public class MainActivity extends Activity { StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia(); if( media != null ) { uri = media.uri; - is_raw = media.path != null && media.path.toLowerCase(Locale.US).endsWith(".dng"); + is_raw = media.filename != null && media.filename.toLowerCase(Locale.US).endsWith(".dng"); } } @@ -2890,7 +3402,7 @@ public class MainActivity extends Activity { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), treeUri.toString()); + editor.putString(PreferenceKeys.SaveLocationSAFPreferenceKey, treeUri.toString()); editor.apply(); if( MyDebug.LOG ) @@ -2908,12 +3420,12 @@ public class MainActivity extends Activity { preview.showToast(null, R.string.saf_permission_failed); // failed - if the user had yet to set a save location, make sure we switch SAF back off SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - String uri = sharedPreferences.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), ""); + String uri = sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, ""); if( uri.length() == 0 ) { if( MyDebug.LOG ) Log.d(TAG, "no SAF save location was set"); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), false); + editor.putBoolean(PreferenceKeys.UsingSAFPreferenceKey, false); editor.apply(); } } @@ -2923,12 +3435,12 @@ public class MainActivity extends Activity { Log.d(TAG, "SAF dialog cancelled"); // cancelled - if the user had yet to set a save location, make sure we switch SAF back off SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - String uri = sharedPreferences.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), ""); + String uri = sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, ""); if( uri.length() == 0 ) { if( MyDebug.LOG ) Log.d(TAG, "no SAF save location was set"); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), false); + editor.putBoolean(PreferenceKeys.UsingSAFPreferenceKey, false); editor.apply(); preview.showToast(null, R.string.saf_cancelled); } @@ -3042,7 +3554,7 @@ public class MainActivity extends Activity { if( MyDebug.LOG ) Log.d(TAG, "changed save_folder to: " + this.applicationInterface.getStorageUtils().getSaveLocation()); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), new_save_location); + editor.putString(PreferenceKeys.SaveLocationPreferenceKey, new_save_location); editor.apply(); this.save_location_history.updateFolderHistory(this.getStorageUtils().getSaveLocation(), true); @@ -3137,6 +3649,7 @@ public class MainActivity extends Activity { final int clear_index = index; items[index++] = getResources().getString(R.string.clear_folder_history); final int new_index = index; + //noinspection UnusedAssignment items[index++] = getResources().getString(R.string.choose_another_folder); alertDialog.setItems(items, new DialogInterface.OnClickListener() { @Override @@ -3210,9 +3723,9 @@ public class MainActivity extends Activity { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); SharedPreferences.Editor editor = sharedPreferences.edit(); if( applicationInterface.getStorageUtils().isUsingSAF() ) - editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), save_folder); + editor.putString(PreferenceKeys.SaveLocationSAFPreferenceKey, save_folder); else - editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), save_folder); + editor.putString(PreferenceKeys.SaveLocationPreferenceKey, save_folder); editor.apply(); history.updateFolderHistory(save_folder, true); // to move new selection to most recent } @@ -3416,7 +3929,7 @@ public class MainActivity extends Activity { if( preview.getCameraController() == null ) return false; if( preview.isVideoHighSpeed() ) { - // manuai ISO/exposure not supported for high speed video mode + // manual ISO/exposure not supported for high speed video mode // it's safer not to allow opening the panel at all (otherwise the user could open it, and switch to manual) return false; } @@ -3432,6 +3945,58 @@ public class MainActivity extends Activity { Log.d(TAG, "cameraSetup"); debug_time = System.currentTimeMillis(); } + + boolean old_want_no_limits = want_no_limits; + this.want_no_limits = false; + if( set_window_insets_listener ) { + Point display_size = new Point(); + Display display = getWindowManager().getDefaultDisplay(); + display.getSize(display_size); + int display_width = Math.max(display_size.x, display_size.y); + int display_height = Math.min(display_size.x, display_size.y); + double display_aspect_ratio = ((double)display_width)/(double)display_height; + double preview_aspect_ratio = preview.getCurrentPreviewAspectRatio(); + if( MyDebug.LOG ) { + Log.d(TAG, "display_aspect_ratio: " + display_aspect_ratio); + Log.d(TAG, "preview_aspect_ratio: " + preview_aspect_ratio); + } + boolean preview_is_wide = preview_aspect_ratio > display_aspect_ratio + 1.0e-5f; + if( test_preview_want_no_limits ) { + preview_is_wide = test_preview_want_no_limits_value; + } + if( preview_is_wide ) { + if( MyDebug.LOG ) + Log.d(TAG, "preview is wide, set want_no_limits"); + this.want_no_limits = true; + + if( !old_want_no_limits ) { + if( MyDebug.LOG ) + Log.d(TAG, "need to change to FLAG_LAYOUT_NO_LIMITS"); + // Ideally we'd just go straight to FLAG_LAYOUT_NO_LIMITS mode, but then all calls to onApplyWindowInsets() + // end up returning a value of 0 for the navigation_gap! So we need to wait until we know the navigation_gap. + if( navigation_gap != 0 ) { + // already have navigation gap, can go straight into no limits mode + if( MyDebug.LOG ) + Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS"); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + // need to layout the UI again due to now taking the navigation gap into account + mainUI.layoutUI(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "but navigation_gap is 0"); + } + } + } + else if( old_want_no_limits && navigation_gap != 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS"); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + // need to layout the UI again due to no longer taking the navigation gap into account + mainUI.layoutUI(); + } + } + if( this.supportsForceVideo4K() && preview.usingCamera2API() ) { if( MyDebug.LOG ) Log.d(TAG, "using Camera2 API, so can disable the force 4K option"); @@ -3510,12 +4075,12 @@ public class MainActivity extends Activity { } } else { - zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this + zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this; in future we might want this similarly for exposure panel } } else { zoomControls.setVisibility(View.GONE); - zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this + zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this; in future we might want this similarly for the exposure panel } if( MyDebug.LOG ) Log.d(TAG, "cameraSetup: time after setting up zoom: " + (System.currentTimeMillis() - debug_time)); @@ -3661,6 +4226,14 @@ public class MainActivity extends Activity { View exposureButton = findViewById(R.id.exposure); exposureButton.setVisibility(supportsExposureButton() && !mainUI.inImmersiveMode() ? View.VISIBLE : View.GONE); + // needed as availability of some icons is per-camera (e.g., flash, RAW) + // for making icons visible, this is done elsewhere in call to MainUI.showGUI() + if( checkDisableGUIIcons() ) { + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: need to layoutUI as we hid some icons"); + mainUI.layoutUI(); + } + // need to update some icons, e.g., white balance and exposure lock due to them being turned off when pause/resuming mainUI.updateOnScreenIcons(); @@ -3732,12 +4305,17 @@ public class MainActivity extends Activity { setManualFocusSeekBarVisibility(is_target_distance); } - void setManualFocusSeekBarVisibility(final boolean is_target_distance) { - SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar); + private boolean showManualFocusSeekbar(final boolean is_target_distance) { boolean is_visible = preview.getCurrentFocusValue() != null && this.getPreview().getCurrentFocusValue().equals("focus_mode_manual2"); if( is_target_distance ) { is_visible = is_visible && (applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.FocusBracketing) && !preview.isVideo(); } + return is_visible; + } + + void setManualFocusSeekBarVisibility(final boolean is_target_distance) { + boolean is_visible = showManualFocusSeekbar(is_target_distance); + SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar); final int visibility = is_visible ? View.VISIBLE : View.GONE; focusSeekBar.setVisibility(visibility); } @@ -3819,6 +4397,7 @@ public class MainActivity extends Activity { return false; // require 256MB just to be safe, due to the large number of images that may be created // also require at least Android 5, for Renderscript + // remember to update the FAQ "Why isn't Panorama supported on my device?" if this changes return( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && large_heap_memory >= 256 && applicationInterface.getGyroSensor().hasSensors() ); //return false; // currently blocked for release } @@ -3925,6 +4504,44 @@ public class MainActivity extends Activity { return changed_auto_stabilise_toast; } + private String getPhotoModeString(MyApplicationInterface.PhotoMode photo_mode, boolean string_for_std) { + String photo_mode_string = null; + switch( photo_mode ) { + case Standard: + if( string_for_std ) + photo_mode_string = getResources().getString(R.string.photo_mode_standard_full); + break; + case DRO: + photo_mode_string = getResources().getString(R.string.photo_mode_dro); + break; + case HDR: + photo_mode_string = getResources().getString(R.string.photo_mode_hdr); + break; + case ExpoBracketing: + photo_mode_string = getResources().getString(R.string.photo_mode_expo_bracketing_full); + break; + case FocusBracketing: { + photo_mode_string = getResources().getString(R.string.photo_mode_focus_bracketing_full); + int n_images = applicationInterface.getFocusBracketingNImagesPref(); + photo_mode_string += " (" + n_images + ")"; + break; + } + case FastBurst: { + photo_mode_string = getResources().getString(R.string.photo_mode_fast_burst_full); + int n_images = applicationInterface.getBurstNImages(); + photo_mode_string += " (" + n_images + ")"; + break; + } + case NoiseReduction: + photo_mode_string = getResources().getString(R.string.photo_mode_noise_reduction_full); + break; + case Panorama: + photo_mode_string = getResources().getString(R.string.photo_mode_panorama_full); + break; + } + return photo_mode_string; + } + /** Displays a toast with information about the current preferences. * If always_show is true, the toast is always displayed; otherwise, we only display * a toast if it's important to notify the user (i.e., unusual non-default settings are @@ -3983,9 +4600,46 @@ public class MainActivity extends Activity { simple = false; } - if( applicationInterface.useVideoLogProfile() && preview.supportsTonemapCurve() ) { - simple = false; - toast_string += "\n" + getResources().getString(R.string.video_log); + { + CameraController.TonemapProfile tonemap_profile = applicationInterface.getVideoTonemapProfile(); + if( tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && preview.supportsTonemapCurve() ) { + if( applicationInterface.getVideoTonemapProfile() != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && preview.supportsTonemapCurve() ) { + int string_id = 0; + switch( tonemap_profile ) { + case TONEMAPPROFILE_REC709: + string_id = R.string.preference_video_rec709; + break; + case TONEMAPPROFILE_SRGB: + string_id = R.string.preference_video_srgb; + break; + case TONEMAPPROFILE_LOG: + string_id = R.string.video_log; + break; + case TONEMAPPROFILE_GAMMA: + string_id = R.string.preference_video_gamma; + break; + case TONEMAPPROFILE_JTVIDEO: + string_id = R.string.preference_video_jtvideo; + break; + case TONEMAPPROFILE_JTLOG: + string_id = R.string.preference_video_jtlog; + break; + case TONEMAPPROFILE_JTLOG2: + string_id = R.string.preference_video_jtlog2; + break; + } + if( string_id != 0 ) { + simple = false; + toast_string += "\n" + getResources().getString(string_id); + if( tonemap_profile == CameraController.TonemapProfile.TONEMAPPROFILE_GAMMA ) { + toast_string += " " + applicationInterface.getVideoProfileGamma(); + } + } + else { + Log.e(TAG, "unknown tonemap_profile: " + tonemap_profile); + } + } + } } boolean record_audio = applicationInterface.getRecordAudioPref(); @@ -3993,7 +4647,7 @@ public class MainActivity extends Activity { toast_string += "\n" + getResources().getString(R.string.audio_disabled); simple = false; } - String max_duration_value = sharedPreferences.getString(PreferenceKeys.getVideoMaxDurationPreferenceKey(), "0"); + String max_duration_value = sharedPreferences.getString(PreferenceKeys.VideoMaxDurationPreferenceKey, "0"); if( max_duration_value.length() > 0 && !max_duration_value.equals("0") ) { String [] entries_array = getResources().getStringArray(R.array.preference_video_max_duration_entries); String [] values_array = getResources().getStringArray(R.array.preference_video_max_duration_values); @@ -4033,36 +4687,7 @@ public class MainActivity extends Activity { toast_string += " " + current_size.width + "x" + current_size.height; } - String photo_mode_string = null; - switch( photo_mode ) { - case DRO: - photo_mode_string = getResources().getString(R.string.photo_mode_dro); - break; - case HDR: - photo_mode_string = getResources().getString(R.string.photo_mode_hdr); - break; - case ExpoBracketing: - photo_mode_string = getResources().getString(R.string.photo_mode_expo_bracketing_full); - break; - case FocusBracketing: { - photo_mode_string = getResources().getString(R.string.photo_mode_focus_bracketing_full); - int n_images = applicationInterface.getFocusBracketingNImagesPref(); - photo_mode_string += " (" + n_images + ")"; - break; - } - case FastBurst: { - photo_mode_string = getResources().getString(R.string.photo_mode_fast_burst_full); - int n_images = applicationInterface.getBurstNImages(); - photo_mode_string += " (" + n_images + ")"; - break; - } - case NoiseReduction: - photo_mode_string = getResources().getString(R.string.photo_mode_noise_reduction_full); - break; - case Panorama: - photo_mode_string = getResources().getString(R.string.photo_mode_panorama_full); - break; - } + String photo_mode_string = getPhotoModeString(photo_mode, false); if( photo_mode_string != null ) { toast_string += (toast_string.length()==0 ? "" : "\n") + getResources().getString(R.string.photo_mode) + ": " + photo_mode_string; simple = false; @@ -4142,7 +4767,7 @@ public class MainActivity extends Activity { simple = false; } } - String timer = sharedPreferences.getString(PreferenceKeys.getTimerPreferenceKey(), "0"); + String timer = sharedPreferences.getString(PreferenceKeys.TimerPreferenceKey, "0"); if( !timer.equals("0") && photo_mode != MyApplicationInterface.PhotoMode.Panorama ) { String [] entries_array = getResources().getStringArray(R.array.preference_timer_entries); String [] values_array = getResources().getStringArray(R.array.preference_timer_values); @@ -4171,9 +4796,19 @@ public class MainActivity extends Activity { if( MyDebug.LOG ) { Log.d(TAG, "toast_string: " + toast_string); Log.d(TAG, "simple?: " + simple); + Log.d(TAG, "push_info_toast_text: " + push_info_toast_text); } - if( !simple || always_show ) - preview.showToast(switch_video_toast, toast_string); + final boolean use_fake_toast = true; + if( !simple || always_show ) { + if( push_info_toast_text != null ) { + toast_string = push_info_toast_text + "\n" + toast_string; + } + preview.showToast(switch_video_toast, toast_string, use_fake_toast); + } + else if( push_info_toast_text != null ) { + preview.showToast(switch_video_toast, push_info_toast_text, use_fake_toast); + } + push_info_toast_text = null; // reset } private void freeAudioListener(boolean wait_until_done) { @@ -4269,10 +4904,19 @@ public class MainActivity extends Activity { } } - void initLocation() { + public void initLocation() { if( MyDebug.LOG ) Log.d(TAG, "initLocation"); - if( !applicationInterface.getLocationSupplier().setupLocationListener() ) { + if( app_is_paused ) { + Log.e(TAG, "initLocation: app is paused!"); + // we shouldn't need this (as we only call initLocation() when active), but just in case we end up here after onPause... + } + else if( camera_in_background ) { + if( MyDebug.LOG ) + Log.d(TAG, "initLocation: camera in background!"); + // we will end up here if app is pause/resumed when camera in background (settings, dialog, etc) + } + else if( !applicationInterface.getLocationSupplier().setupLocationListener() ) { if( MyDebug.LOG ) Log.d(TAG, "location permission not available, so request permission"); permissionHandler.requestLocationPermission(); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index a10cc05df37f926ff51e8d79d58347a89847c5ab..1ae3a3eaaceab2caaa581abc234e0a9683febf83 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -139,9 +139,15 @@ public class MyApplicationInterface extends BasicApplicationInterface { private int cameraId = cameraId_default; private final static String nr_mode_default = "preference_nr_mode_normal"; private String nr_mode = nr_mode_default; + private final static float aperture_default = -1.0f; + private float aperture = aperture_default; // camera properties that aren't saved even in the bundle; these should be initialised/reset in reset() private int zoom_factor; // don't save zoom, as doing so tends to confuse users; other camera applications don't seem to save zoom when pause/resuming + // for testing: + public volatile int test_n_videos_scanned; + public volatile int test_max_mp; + MyApplicationInterface(MainActivity main_activity, Bundle savedInstanceState) { long debug_time = 0; if( MyDebug.LOG ) { @@ -162,7 +168,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { this.imageSaver = new ImageSaver(main_activity); this.imageSaver.start(); - this.reset(); + this.reset(false); if( savedInstanceState != null ) { // load the things we saved in onSaveInstanceState(). if( MyDebug.LOG ) @@ -174,6 +180,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { nr_mode = savedInstanceState.getString("nr_mode", nr_mode_default); if( MyDebug.LOG ) Log.d(TAG, "found nr_mode: " + nr_mode); + aperture = savedInstanceState.getFloat("aperture", aperture_default); + if( MyDebug.LOG ) + Log.d(TAG, "found aperture: " + aperture); } if( MyDebug.LOG ) @@ -193,6 +202,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) Log.d(TAG, "save nr_mode: " + nr_mode); state.putString("nr_mode", nr_mode); + if( MyDebug.LOG ) + Log.d(TAG, "save aperture: " + aperture); + state.putFloat("aperture", aperture); } void onDestroy() { @@ -242,11 +254,23 @@ public class MyApplicationInterface extends BasicApplicationInterface { return false; } + /** If adding extra calls to this, consider whether explicit user permission is required, and whether + * privacy policy needs updating. + * Returns null if location not available. + */ @Override public Location getLocation() { return locationSupplier.getLocation(); } + /** If adding extra calls to this, consider whether explicit user permission is required, and whether + * privacy policy needs updating. + * Returns null if location not available. + */ + public Location getLocation(LocationSupplier.LocationInfo locationInfo) { + return locationSupplier.getLocation(locationInfo); + } + @Override public int createOutputVideoMethod() { String action = main_activity.getIntent().getAction(); @@ -448,14 +472,17 @@ public class MyApplicationInterface extends BasicApplicationInterface { } @Override - public Pair getCameraResolutionPref() { - if( getPhotoMode() == PhotoMode.Panorama ) { + public Pair getCameraResolutionPref(CameraResolutionConstraints constraints) { + PhotoMode photo_mode = getPhotoMode(); + if( photo_mode == PhotoMode.Panorama ) { CameraController.Size best_size = choosePanoramaResolution(main_activity.getPreview().getSupportedPictureSizes(false)); return new Pair<>(best_size.width, best_size.height); } + String resolution_value = sharedPreferences.getString(PreferenceKeys.getResolutionPreferenceKey(cameraId), ""); if( MyDebug.LOG ) Log.d(TAG, "resolution_value: " + resolution_value); + Pair result = null; if( resolution_value.length() > 0 ) { // parse the saved size, and make sure it is still valid int index = resolution_value.indexOf(' '); @@ -477,7 +504,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { int resolution_h = Integer.parseInt(resolution_h_s); if( MyDebug.LOG ) Log.d(TAG, "resolution_h: " + resolution_h); - return new Pair<>(resolution_w, resolution_h); + result = new Pair<>(resolution_w, resolution_h); } catch(NumberFormatException exception) { if( MyDebug.LOG ) @@ -485,7 +512,19 @@ public class MyApplicationInterface extends BasicApplicationInterface { } } } - return null; + + if( photo_mode == PhotoMode.NoiseReduction || photo_mode == PhotoMode.HDR ) { + // set a maximum resolution for modes that require decompressing multiple images for processing, + // due to risk of running out of memory! + constraints.has_max_mp = true; + constraints.max_mp = 22000000; // max of 22MP + //constraints.max_mp = 7800000; // test! + if( main_activity.is_test && test_max_mp != 0 ) { + constraints.max_mp = test_max_mp; + } + } + + return result; } /** getImageQualityPref() returns the image quality used for the Camera Controller for taking a @@ -586,12 +625,12 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public boolean getVideoStabilizationPref() { - return sharedPreferences.getBoolean(PreferenceKeys.getVideoStabilizationPreferenceKey(), false); + return sharedPreferences.getBoolean(PreferenceKeys.VideoStabilizationPreferenceKey, false); } @Override public boolean getForce4KPref() { - return cameraId == 0 && sharedPreferences.getBoolean(PreferenceKeys.getForceVideo4KPreferenceKey(), false) && main_activity.supportsForceVideo4K(); + return cameraId == 0 && sharedPreferences.getBoolean(PreferenceKeys.ForceVideo4KPreferenceKey, false) && main_activity.supportsForceVideo4K(); } @Override @@ -601,7 +640,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public String getVideoBitratePref() { - return sharedPreferences.getString(PreferenceKeys.getVideoBitratePreferenceKey(), "default"); + return sharedPreferences.getString(PreferenceKeys.VideoBitratePreferenceKey, "default"); } @Override @@ -720,30 +759,48 @@ public class MyApplicationInterface extends BasicApplicationInterface { } @Override - public boolean useVideoLogProfile() { + public CameraController.TonemapProfile getVideoTonemapProfile() { String video_log = sharedPreferences.getString(PreferenceKeys.VideoLogPreferenceKey, "off"); - // only return true for values recognised by getVideoLogProfileStrength() + // only return TONEMAPPROFILE_LOG for values recognised by getVideoLogProfileStrength() switch( video_log ) { case "off": - return false; + return CameraController.TonemapProfile.TONEMAPPROFILE_OFF; + case "rec709": + return CameraController.TonemapProfile.TONEMAPPROFILE_REC709; + case "srgb": + return CameraController.TonemapProfile.TONEMAPPROFILE_SRGB; case "fine": case "low": case "medium": case "strong": case "extra_strong": - return true; + return CameraController.TonemapProfile.TONEMAPPROFILE_LOG; + case "gamma": + return CameraController.TonemapProfile.TONEMAPPROFILE_GAMMA; + case "jtvideo": + return CameraController.TonemapProfile.TONEMAPPROFILE_JTVIDEO; + case "jtlog": + return CameraController.TonemapProfile.TONEMAPPROFILE_JTLOG; + case "jtlog2": + return CameraController.TonemapProfile.TONEMAPPROFILE_JTLOG2; } - return false; + return CameraController.TonemapProfile.TONEMAPPROFILE_OFF; } @Override public float getVideoLogProfileStrength() { String video_log = sharedPreferences.getString(PreferenceKeys.VideoLogPreferenceKey, "off"); - // remember to update useVideoLogProfile() if adding/changing modes + // remember to update getVideoTonemapProfile() if adding/changing modes switch( video_log ) { case "off": + case "rec709": + case "srgb": + case "gamma": + case "jtvideo": + case "jtlog": + case "jtlog2": return 0.0f; - case "fine": + /*case "fine": return 1.0f; case "low": return 5.0f; @@ -751,12 +808,42 @@ public class MyApplicationInterface extends BasicApplicationInterface { return 10.0f; case "strong": return 100.0f; + case "extra_strong": + return 500.0f;*/ + // need a range of values as behaviour can vary between devices - e.g., "fine" has more effect on Nexus 6 than + // other devices such as OnePlus 3T or Galaxy S10e + // recalibrated in v1.48 to correspond to improvements made in CameraController2 + case "fine": + return 10.0f; + case "low": + return 32.0f; + case "medium": + return 100.0f; + case "strong": + return 224.0f; case "extra_strong": return 500.0f; } return 0.0f; } + @Override + public float getVideoProfileGamma() { + String gamma_value = sharedPreferences.getString(PreferenceKeys.VideoProfileGammaPreferenceKey, "2.2"); + float gamma = 0.0f; + try { + gamma = Float.parseFloat(gamma_value); + if( MyDebug.LOG ) + Log.d(TAG, "gamma: " + gamma); + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse gamma value: " + gamma_value); + e.printStackTrace(); + } + return gamma; + } + @Override public long getVideoMaxDurationPref() { String action = main_activity.getIntent().getAction(); @@ -771,7 +858,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { } } - String video_max_duration_value = sharedPreferences.getString(PreferenceKeys.getVideoMaxDurationPreferenceKey(), "0"); + String video_max_duration_value = sharedPreferences.getString(PreferenceKeys.VideoMaxDurationPreferenceKey, "0"); long video_max_duration; try { video_max_duration = (long)Integer.parseInt(video_max_duration_value) * 1000; @@ -787,7 +874,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public int getVideoRestartTimesPref() { - String restart_value = sharedPreferences.getString(PreferenceKeys.getVideoRestartPreferenceKey(), "0"); + String restart_value = sharedPreferences.getString(PreferenceKeys.VideoRestartPreferenceKey, "0"); int remaining_restart_video; try { remaining_restart_video = Integer.parseInt(restart_value); @@ -817,7 +904,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { } } - String video_max_filesize_value = sharedPreferences.getString(PreferenceKeys.getVideoMaxFileSizePreferenceKey(), "0"); + String video_max_filesize_value = sharedPreferences.getString(PreferenceKeys.VideoMaxFileSizePreferenceKey, "0"); long video_max_filesize; try { video_max_filesize = Long.parseLong(video_max_filesize_value); @@ -844,7 +931,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { } } - return sharedPreferences.getBoolean(PreferenceKeys.getVideoRestartMaxFileSizePreferenceKey(), true); + return sharedPreferences.getBoolean(PreferenceKeys.VideoRestartMaxFileSizePreferenceKey, true); } @Override @@ -931,12 +1018,12 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public boolean getVideoFlashPref() { - return sharedPreferences.getBoolean(PreferenceKeys.getVideoFlashPreferenceKey(), false); + return sharedPreferences.getBoolean(PreferenceKeys.VideoFlashPreferenceKey, false); } @Override public boolean getVideoLowPowerCheckPref() { - return sharedPreferences.getBoolean(PreferenceKeys.getVideoLowPowerCheckPreferenceKey(), true); + return sharedPreferences.getBoolean(PreferenceKeys.VideoLowPowerCheckPreferenceKey, true); } @Override @@ -946,14 +1033,14 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public String getPreviewRotationPref() { - return sharedPreferences.getString(PreferenceKeys.getRotatePreviewPreferenceKey(), "0"); + return sharedPreferences.getString(PreferenceKeys.RotatePreviewPreferenceKey, "0"); } @Override public String getLockOrientationPref() { if( getPhotoMode() == PhotoMode.Panorama ) return "portrait"; // for now panorama only supports portrait - return sharedPreferences.getString(PreferenceKeys.getLockOrientationPreferenceKey(), "none"); + return sharedPreferences.getString(PreferenceKeys.LockOrientationPreferenceKey, "none"); } @Override @@ -1001,19 +1088,19 @@ public class MyApplicationInterface extends BasicApplicationInterface { public boolean getShutterSoundPref() { if( getPhotoMode() == PhotoMode.Panorama ) return false; - return sharedPreferences.getBoolean(PreferenceKeys.getShutterSoundPreferenceKey(), true); + return sharedPreferences.getBoolean(PreferenceKeys.ShutterSoundPreferenceKey, true); } @Override public boolean getStartupFocusPref() { - return sharedPreferences.getBoolean(PreferenceKeys.getStartupFocusPreferenceKey(), true); + return sharedPreferences.getBoolean(PreferenceKeys.StartupFocusPreferenceKey, true); } @Override public long getTimerPref() { if( getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama ) return 0; // don't support timer with panorama - String timer_value = sharedPreferences.getString(PreferenceKeys.getTimerPreferenceKey(), "0"); + String timer_value = sharedPreferences.getString(PreferenceKeys.TimerPreferenceKey, "0"); long timer_delay; try { timer_delay = (long)Integer.parseInt(timer_value) * 1000; @@ -1031,12 +1118,12 @@ public class MyApplicationInterface extends BasicApplicationInterface { public String getRepeatPref() { if( getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama ) return "1"; // don't support repeat with panorama - return sharedPreferences.getString(PreferenceKeys.getRepeatModePreferenceKey(), "1"); + return sharedPreferences.getString(PreferenceKeys.RepeatModePreferenceKey, "1"); } @Override public long getRepeatIntervalPref() { - String timer_value = sharedPreferences.getString(PreferenceKeys.getRepeatIntervalPreferenceKey(), "0"); + String timer_value = sharedPreferences.getString(PreferenceKeys.RepeatIntervalPreferenceKey, "0"); long timer_delay; try { float timer_delay_s = Float.parseFloat(timer_value); @@ -1069,17 +1156,17 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public boolean getRecordAudioPref() { - return sharedPreferences.getBoolean(PreferenceKeys.getRecordAudioPreferenceKey(), true); + return sharedPreferences.getBoolean(PreferenceKeys.RecordAudioPreferenceKey, true); } @Override public String getRecordAudioChannelsPref() { - return sharedPreferences.getString(PreferenceKeys.getRecordAudioChannelsPreferenceKey(), "audio_default"); + return sharedPreferences.getString(PreferenceKeys.RecordAudioChannelsPreferenceKey, "audio_default"); } @Override public String getRecordAudioSourcePref() { - return sharedPreferences.getString(PreferenceKeys.getRecordAudioSourcePreferenceKey(), "audio_src_camcorder"); + return sharedPreferences.getString(PreferenceKeys.RecordAudioSourcePreferenceKey, "audio_src_camcorder"); } public boolean getAutoStabilisePref() { @@ -1087,6 +1174,25 @@ public class MyApplicationInterface extends BasicApplicationInterface { return auto_stabilise && main_activity.supportsAutoStabilise(); } + /** Returns the alpha value to use for ghost image, as a number from 0 to 255. + * Note that we store the preference as a percentage from 0 to 100, but scale this to 0 to 255. + */ + public int getGhostImageAlpha() { + String ghost_image_alpha_value = sharedPreferences.getString(PreferenceKeys.GhostImageAlphaPreferenceKey, "50"); + int ghost_image_alpha; + try { + ghost_image_alpha = Integer.parseInt(ghost_image_alpha_value); + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse ghost_image_alpha_value: " + ghost_image_alpha_value); + e.printStackTrace(); + ghost_image_alpha = 50; + } + ghost_image_alpha = (int)(ghost_image_alpha*2.55f+0.1f); + return ghost_image_alpha; + } + public String getStampPref() { return sharedPreferences.getString(PreferenceKeys.StampPreferenceKey, "preference_stamp_no"); } @@ -1329,6 +1435,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { public NRModePref getNRModePref() { /*if( MyDebug.LOG ) Log.d(TAG, "nr_mode: " + nr_mode);*/ + //noinspection SwitchStatementWithTooFewBranches switch( nr_mode ) { case "preference_nr_mode_low_light": return NRModePref.NRMODE_LOW_LIGHT; @@ -1336,6 +1443,15 @@ public class MyApplicationInterface extends BasicApplicationInterface { return NRModePref.NRMODE_NORMAL; } + public void setAperture(float aperture) { + this.aperture = aperture; + } + + @Override + public float getAperturePref() { + return aperture; + } + @Override public int getExpoBracketingNImagesPref() { if( MyDebug.LOG ) @@ -1523,6 +1639,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { */ boolean isRawOnly(PhotoMode photo_mode) { if( isRawAllowed(photo_mode) ) { + //noinspection SwitchStatementWithTooFewBranches switch( sharedPreferences.getString(PreferenceKeys.RawPreferenceKey, "preference_raw_no") ) { case "preference_raw_only": return true; @@ -1604,7 +1721,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { main_activity.getMainUI().setTakePhotoIcon(); View cancelPanoramaButton = main_activity.findViewById(R.id.cancel_panorama); cancelPanoramaButton.setVisibility(View.VISIBLE); - main_activity.getMainUI().clearSeekBar(); // close seekbars if open (popup is already closed when taking a photo) + main_activity.getMainUI().closeExposureUI(); // close seekbars if open (popup is already closed when taking a photo) // taking the photo will end up calling MainUI.showGUI(), which will hide the other on-screen icons } @@ -1737,7 +1854,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public void touchEvent(MotionEvent event) { - main_activity.getMainUI().clearSeekBar(); + main_activity.getMainUI().closeExposureUI(); main_activity.getMainUI().closePopup(); if( main_activity.usingKitKatImmersiveMode() ) { main_activity.setImmersiveMode(false); @@ -1746,7 +1863,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public void startingVideo() { - if( sharedPreferences.getBoolean(PreferenceKeys.getLockVideoPreferenceKey(), false) ) { + if( sharedPreferences.getBoolean(PreferenceKeys.LockVideoPreferenceKey, false) ) { main_activity.lockScreen(); } main_activity.stopAudioListeners(); // important otherwise MediaRecorder will fail to start() if we have an audiolistener! Also don't want to have the speech recognizer going off @@ -1842,7 +1959,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) { Log.d(TAG, "date_stamp: " + date_stamp); Log.d(TAG, "time_stamp: " + time_stamp); - Log.d(TAG, "gps_stamp: " + gps_stamp); + // don't log gps_stamp, in case of privacy! } String datetime_stamp = ""; @@ -1871,8 +1988,8 @@ public class MyApplicationInterface extends BasicApplicationInterface { List
addresses = geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); if( addresses != null && addresses.size() > 0 ) { address = addresses.get(0); + // don't log address, in case of privacy! if( MyDebug.LOG ) { - Log.d(TAG, "address: " + address); Log.d(TAG, "max line index: " + address.getMaxAddressLineIndex()); } } @@ -1906,8 +2023,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "not displaying gps coords, but need to display geo direction"); gps_stamp = main_activity.getTextFormatter().getGPSString(preference_stamp_gpsformat, preference_units_distance, false, null, store_geo_direction && main_activity.getPreview().hasGeoDirection(), geo_direction); if( gps_stamp.length() > 0 ) { - if( MyDebug.LOG ) - Log.d(TAG, "gps_stamp is now: " + gps_stamp); + // don't log gps_stamp, in case of privacy! subtitles.append(gps_stamp).append("\n"); } } @@ -2028,24 +2144,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { subtitleVideoTimerTask = null; } - boolean done = false; - if( video_method == VIDEOMETHOD_FILE ) { - if( filename != null ) { - File file = new File(filename); - storageUtils.broadcastFile(file, false, true, true); - done = true; - } - } - else { - if( uri != null ) { - // see note in onPictureTaken() for where we call broadcastFile for SAF photos - File real_file = storageUtils.broadcastUri(uri, false, true, true); - if( real_file != null ) { - main_activity.test_last_saved_image = real_file.getAbsolutePath(); - } - done = true; - } - } + boolean done = broadcastVideo(video_method, uri, filename); if( MyDebug.LOG ) Log.d(TAG, "done? " + done); @@ -2133,6 +2232,67 @@ public class MyApplicationInterface extends BasicApplicationInterface { } } + @Override + public void restartedVideo(final int video_method, final Uri uri, final String filename) { + if( MyDebug.LOG ) { + Log.d(TAG, "restartedVideo"); + Log.d(TAG, "video_method " + video_method); + Log.d(TAG, "uri " + uri); + Log.d(TAG, "filename " + filename); + } + broadcastVideo(video_method, uri, filename); + } + + private boolean broadcastVideo(final int video_method, final Uri uri, final String filename) { + if( MyDebug.LOG ) { + Log.d(TAG, "broadcastVideo"); + Log.d(TAG, "video_method " + video_method); + Log.d(TAG, "uri " + uri); + Log.d(TAG, "filename " + filename); + } + boolean done = false; + if( video_method == VIDEOMETHOD_FILE ) { + if( filename != null ) { + File file = new File(filename); + storageUtils.broadcastFile(file, false, true, true); + done = true; + } + } + else { + if( uri != null ) { + // see note in onPictureTaken() for where we call broadcastFile for SAF photos + File real_file = storageUtils.broadcastUri(uri, false, true, true); + if( real_file != null ) { + main_activity.test_last_saved_image = real_file.getAbsolutePath(); + } + done = true; + } + } + if( done ) { + test_n_videos_scanned++; + if( MyDebug.LOG ) + Log.d(TAG, "test_n_videos_scanned is now: " + test_n_videos_scanned); + } + return done; + } + + @Override + public void deleteUnusedVideo(final int video_method, final Uri uri, final String filename) { + if( MyDebug.LOG ) { + Log.d(TAG, "deleteUnusedVideo"); + Log.d(TAG, "video_method " + video_method); + Log.d(TAG, "uri " + uri); + Log.d(TAG, "filename " + filename); + } + if( video_method == VIDEOMETHOD_FILE ) { + trashImage(false, uri, filename, false); + } + else if( video_method == VIDEOMETHOD_SAF ) { + trashImage(true, uri, filename, false); + } + // else can't delete Uri + } + @Override public void onVideoInfo(int what, int extra) { // we don't show a toast for MEDIA_RECORDER_INFO_MAX_DURATION_REACHED - conflicts with "n repeats to go" toast from Preview @@ -2336,7 +2496,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) Log.d(TAG, "cameraClosed"); this.stopPanorama(true); - main_activity.getMainUI().clearSeekBar(); + main_activity.getMainUI().closeExposureUI(); main_activity.getMainUI().destroyPopup(); // need to close popup - and when camera reopened, it may have different settings drawPreview.clearContinuousFocusMove(); } @@ -2357,13 +2517,13 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "timerBeep()"); Log.d(TAG, "remaining_time: " + remaining_time); } - if( sharedPreferences.getBoolean(PreferenceKeys.getTimerBeepPreferenceKey(), true) ) { + if( sharedPreferences.getBoolean(PreferenceKeys.TimerBeepPreferenceKey, true) ) { if( MyDebug.LOG ) Log.d(TAG, "play beep!"); boolean is_last = remaining_time <= 1000; main_activity.getSoundPoolManager().playSound(is_last ? R.raw.mybeep_hi : R.raw.mybeep); } - if( sharedPreferences.getBoolean(PreferenceKeys.getTimerSpeakPreferenceKey(), false) ) { + if( sharedPreferences.getBoolean(PreferenceKeys.TimerSpeakPreferenceKey, false) ) { if( MyDebug.LOG ) Log.d(TAG, "speak countdown!"); int remaining_time_s = (int)(remaining_time/1000); @@ -2384,8 +2544,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) Log.d(TAG, "switchToCamera: " + front_facing); int n_cameras = main_activity.getPreview().getCameraControllerManager().getNumberOfCameras(); + CameraController.Facing want_facing = front_facing ? CameraController.Facing.FACING_FRONT : CameraController.Facing.FACING_BACK; for(int i=0;i 0 ) { @@ -1107,6 +1166,8 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared about_string.append(getString(supports_hdr ? R.string.about_available : R.string.about_not_available)); about_string.append("\nPanorama?: "); about_string.append(getString(supports_panorama ? R.string.about_available : R.string.about_not_available)); + about_string.append("\nGyro sensors?: "); + about_string.append(getString(has_gyro_sensors ? R.string.about_available : R.string.about_not_available)); about_string.append("\nExpo?: "); about_string.append(getString(supports_expo_bracketing ? R.string.about_available : R.string.about_not_available)); about_string.append("\nExpo compensation?: "); @@ -1141,14 +1202,22 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared about_string.append(" to "); about_string.append(white_balance_temperature_max); } + about_string.append("\nOptical stabilization?: "); + about_string.append(getString(supports_optical_stabilization ? R.string.about_available : R.string.about_not_available)); + about_string.append("\nOptical stabilization enabled?: "); + about_string.append(optical_stabilization_enabled); about_string.append("\nVideo stabilization?: "); about_string.append(getString(supports_video_stabilization ? R.string.about_available : R.string.about_not_available)); + about_string.append("\nVideo stabilization enabled?: "); + about_string.append(video_stabilization_enabled); + about_string.append("\nTonemap curve?: "); + about_string.append(getString(supports_tonemap_curve ? R.string.about_available : R.string.about_not_available)); about_string.append("\nTonemap max curve points: "); about_string.append(tonemap_max_curve_points); about_string.append("\nCan disable shutter sound?: "); about_string.append(getString(can_disable_shutter_sound ? R.string.about_available : R.string.about_not_available)); - about_string.append("\nCamera view angle: " + camera_view_angle_x + " , " + camera_view_angle_y); + about_string.append("\nCamera view angle: ").append(camera_view_angle_x).append(" , ").append(camera_view_angle_y); about_string.append("\nFlash modes: "); String [] flash_values = bundle.getStringArray("flash_values"); @@ -1240,11 +1309,11 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared about_string.append(magnetic_accuracy); about_string.append("\nUsing SAF?: "); - about_string.append(sharedPreferences.getBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), false)); - String save_location = sharedPreferences.getString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCamera"); + about_string.append(sharedPreferences.getBoolean(PreferenceKeys.UsingSAFPreferenceKey, false)); + String save_location = sharedPreferences.getString(PreferenceKeys.SaveLocationPreferenceKey, "OpenCamera"); about_string.append("\nSave Location: "); about_string.append(save_location); - String save_location_saf = sharedPreferences.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), ""); + String save_location_saf = sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, ""); about_string.append("\nSave Location SAF: "); about_string.append(save_location_saf); @@ -1445,6 +1514,61 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared } }); } + + setupDependencies(); + } + + /** Programmatically set up dependencies for preference types (e.g., ListPreference) that don't + * support this in xml (such as SwitchPreference and CheckBoxPreference). + */ + private void setupDependencies() { + // set up dependency for preference_audio_noise_control_sensitivity on preference_audio_control + ListPreference pref = (ListPreference)findPreference("preference_audio_control"); + if( pref != null ) { // may be null if preference not supported + pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference arg0, Object newValue) { + String value = newValue.toString(); + setAudioNoiseControlSensitivityDependency(value); + return true; + } + }); + setAudioNoiseControlSensitivityDependency(pref.getValue()); // ensure dependency is enabled/disabled as required for initial value + } + + // set up dependency for preference_video_profile_gamma on preference_video_log + pref = (ListPreference)findPreference("preference_video_log"); + if( pref != null ) { // may be null if preference not supported + pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference arg0, Object newValue) { + String value = newValue.toString(); + setVideoProfileGammaDependency(value); + return true; + } + }); + setVideoProfileGammaDependency(pref.getValue()); // ensure dependency is enabled/disabled as required for initial value + } + } + + private void setAudioNoiseControlSensitivityDependency(String newValue) { + Preference dependent = findPreference("preference_audio_noise_control_sensitivity"); + if( dependent != null ) { // just in case + boolean enable_dependent = "noise".equals(newValue); + if( MyDebug.LOG ) + Log.d(TAG, "clicked audio control: " + newValue + " enable_dependent: " + enable_dependent); + dependent.setEnabled(enable_dependent); + } + } + + private void setVideoProfileGammaDependency(String newValue) { + Preference dependent = findPreference("preference_video_profile_gamma"); + if( dependent != null ) { // just in case + boolean enable_dependent = "gamma".equals(newValue); + if( MyDebug.LOG ) + Log.d(TAG, "clicked video log: " + newValue + " enable_dependent: " + enable_dependent); + dependent.setEnabled(enable_dependent); + } } /** Removes an entry and value pair from a ListPreference, if it exists. diff --git a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java index 21ce7fad651aa4478b293dab9a149b14f34cf6d2..b5bb016213fdf07a78f7bcf0ef30f90212f77c78 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java @@ -218,7 +218,7 @@ public class PanoramaProcessor { * RGBA_8888. */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private List createLaplacianPyramid(ScriptC_pyramid_blending script,Bitmap bitmap, int n_levels, String name) { + private List createLaplacianPyramid(ScriptC_pyramid_blending script, Bitmap bitmap, int n_levels, @SuppressWarnings("unused") String name) { if( MyDebug.LOG ) Log.d(TAG, "createLaplacianPyramid"); long time_s = 0; @@ -497,6 +497,7 @@ public class PanoramaProcessor { } } + @SuppressWarnings("unused") @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void saveAllocation(String name, Allocation allocation) { Bitmap bitmap; @@ -543,13 +544,13 @@ public class PanoramaProcessor { bitmap.recycle(); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + /*@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void savePyramid(String name, List pyramid) { for(int i=0;i matches, int st_indx, int nd_indx, int feature_descriptor_radius, List bitmaps, int [] pixels0, int [] pixels1) { + private static void computeDistancesBetweenMatches(List matches, int st_indx, int nd_indx, int feature_descriptor_radius, @SuppressWarnings("unused") List bitmaps, int [] pixels0, int [] pixels1) { final int wid = 2*feature_descriptor_radius+1; final int wid2 = wid*wid; for(int indx=st_indx;indx power ) power *= 2; return power; - } + }*/ private static int nextMultiple(int value, int multiple) { int remainder = value % multiple; @@ -2478,6 +2486,7 @@ public class PanoramaProcessor { return ratio_brightnesses; } + @SuppressWarnings("unused") @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void adjustExposures(List bitmaps, long time_s) { List histogramInfos = new ArrayList<>(); @@ -2821,6 +2830,7 @@ public class PanoramaProcessor { int align_y = 0; int dst_offset_x = dst_offset_x_values.get(i); + //noinspection UnusedAssignment boolean free_bitmap = false; int shift_stop_x = align_x; int centre_shift_x; diff --git a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessorException.java b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessorException.java index e41d3525fe128ef9958e462e0fb30244feb374e0..c5dfc178a7b5207b36c0791f6e86bb58955401f6 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessorException.java +++ b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessorException.java @@ -2,6 +2,7 @@ package net.sourceforge.opencamera; /** Exception for PanoramaProcessor class. */ +@SuppressWarnings("WeakerAccess") public class PanoramaProcessorException extends Exception { final static public int INVALID_N_IMAGES = 0; // the supplied number of images is not supported final static public int UNEQUAL_SIZES = 1; // images not of the same resolution diff --git a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java index c28629318d4921151e5916dc198cc74209e19a22..cc8732db0d865d9abb0dd714d5c87e5bb13e0a6b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java +++ b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java @@ -5,8 +5,6 @@ package net.sourceforge.opencamera; public class PreferenceKeys { // must be static, to safely call from other Activities - // arguably the static methods here that don't receive an argument could just be static final strings? Though we may want to change some of them to be cameraId-specific in future - /** If this preference is set, no longer show the intro dialog. */ public static final String FirstTimePreferenceKey = "done_first_time"; @@ -190,45 +188,29 @@ public class PreferenceKeys { public static final String DimWhenDisconnectedPreferenceKey = "preference_remote_disconnect_screen_dim"; - public static String getShowWhenLockedPreferenceKey() { - return "preference_show_when_locked"; - } + public static final String ShowWhenLockedPreferenceKey = "preference_show_when_locked"; - public static String getStartupFocusPreferenceKey() { - return "preference_startup_focus"; - } + public static final String AllowLongPressPreferenceKey = "preference_allow_long_press"; - public static String getKeepDisplayOnPreferenceKey() { - return "preference_keep_display_on"; - } + public static final String StartupFocusPreferenceKey = "preference_startup_focus"; - public static String getMaxBrightnessPreferenceKey() { - return "preference_max_brightness"; - } + public static final String MultiCamButtonPreferenceKey = "preference_multi_cam_button"; - public static String getUsingSAFPreferenceKey() { - return "preference_using_saf"; - } + public static final String KeepDisplayOnPreferenceKey = "preference_keep_display_on"; - public static String getSaveLocationPreferenceKey() { - return "preference_save_location"; - } + public static final String MaxBrightnessPreferenceKey = "preference_max_brightness"; - public static String getSaveLocationSAFPreferenceKey() { - return "preference_save_location_saf"; - } + public static final String UsingSAFPreferenceKey = "preference_using_saf"; - public static String getSavePhotoPrefixPreferenceKey() { - return "preference_save_photo_prefix"; - } + public static final String SaveLocationPreferenceKey = "preference_save_location"; - public static String getSaveVideoPrefixPreferenceKey() { - return "preference_save_video_prefix"; - } + public static final String SaveLocationSAFPreferenceKey = "preference_save_location_saf"; - public static String getSaveZuluTimePreferenceKey() { - return "preference_save_zulu_time"; - } + public static final String SavePhotoPrefixPreferenceKey = "preference_save_photo_prefix"; + + public static final String SaveVideoPrefixPreferenceKey = "preference_save_video_prefix"; + + public static final String SaveZuluTimePreferenceKey = "preference_save_zulu_time"; public static final String ShowZoomControlsPreferenceKey = "preference_show_zoom_controls"; @@ -262,6 +244,10 @@ public class PreferenceKeys { public static final String ZebraStripesPreferenceKey = "preference_zebra_stripes"; + public static final String ZebraStripesForegroundColorPreferenceKey = "preference_zebra_stripes_foreground_color"; + + public static final String ZebraStripesBackgroundColorPreferenceKey = "preference_zebra_stripes_background_color"; + public static final String FocusPeakingPreferenceKey = "preference_focus_peaking"; public static final String FocusPeakingColorPreferenceKey = "preference_focus_peaking_color"; @@ -286,6 +272,8 @@ public class PreferenceKeys { public static final String ShowTimePreferenceKey = "preference_show_time"; + public static final String ShowCameraIDPreferenceKey = "preference_show_camera_id"; + public static final String ShowBatteryPreferenceKey = "preference_show_battery"; public static final String ShowGridPreferenceKey = "preference_grid"; @@ -298,19 +286,15 @@ public class PreferenceKeys { public static final String GhostSelectedImageSAFPreferenceKey = "preference_ghost_selected_image_saf"; - public static String getVideoStabilizationPreferenceKey() { - return "preference_video_stabilization"; - } + public static final String GhostImageAlphaPreferenceKey = "ghost_image_alpha"; - public static String getForceVideo4KPreferenceKey() { - return "preference_force_video_4k"; - } + public static final String VideoStabilizationPreferenceKey = "preference_video_stabilization"; + + public static final String ForceVideo4KPreferenceKey = "preference_force_video_4k"; public static final String VideoFormatPreferenceKey = "preference_video_output_format"; - public static String getVideoBitratePreferenceKey() { - return "preference_video_bitrate"; - } + public static final String VideoBitratePreferenceKey = "preference_video_bitrate"; public static String getVideoFPSPreferenceKey(int cameraId) { // for cameraId==0, we return preference_video_fps instead of preference_video_fps_0, for @@ -324,83 +308,50 @@ public class PreferenceKeys { public static final String VideoLogPreferenceKey = "preference_video_log"; - public static String getVideoMaxDurationPreferenceKey() { - return "preference_video_max_duration"; - } + public static final String VideoProfileGammaPreferenceKey = "preference_video_profile_gamma"; - public static String getVideoRestartPreferenceKey() { - return "preference_video_restart"; - } + public static final String VideoMaxDurationPreferenceKey = "preference_video_max_duration"; - public static String getVideoMaxFileSizePreferenceKey() { - return "preference_video_max_filesize"; - } + public static final String VideoRestartPreferenceKey = "preference_video_restart"; - public static String getVideoRestartMaxFileSizePreferenceKey() { - return "preference_video_restart_max_filesize"; - } + public static final String VideoMaxFileSizePreferenceKey = "preference_video_max_filesize"; - public static String getVideoFlashPreferenceKey() { - return "preference_video_flash"; - } + public static final String VideoRestartMaxFileSizePreferenceKey = "preference_video_restart_max_filesize"; - public static String getVideoLowPowerCheckPreferenceKey() { - return "preference_video_low_power_check"; - } + public static final String VideoFlashPreferenceKey = "preference_video_flash"; - public static String getLockVideoPreferenceKey() { - return "preference_lock_video"; - } + public static final String VideoLowPowerCheckPreferenceKey = "preference_video_low_power_check"; - public static String getRecordAudioPreferenceKey() { - return "preference_record_audio"; - } + public static final String LockVideoPreferenceKey = "preference_lock_video"; - public static String getRecordAudioChannelsPreferenceKey() { - return "preference_record_audio_channels"; - } + public static final String RecordAudioPreferenceKey = "preference_record_audio"; - public static String getRecordAudioSourcePreferenceKey() { - return "preference_record_audio_src"; - } + public static final String RecordAudioChannelsPreferenceKey = "preference_record_audio_channels"; + + public static final String RecordAudioSourcePreferenceKey = "preference_record_audio_src"; public static final String PreviewSizePreferenceKey = "preference_preview_size"; - public static String getRotatePreviewPreferenceKey() { - return "preference_rotate_preview"; - } + public static final String RotatePreviewPreferenceKey = "preference_rotate_preview"; - public static String getLockOrientationPreferenceKey() { - return "preference_lock_orientation"; - } + public static final String LockOrientationPreferenceKey = "preference_lock_orientation"; - public static String getTimerPreferenceKey() { - return "preference_timer"; - } + public static final String TimerPreferenceKey = "preference_timer"; - public static String getTimerBeepPreferenceKey() { - return "preference_timer_beep"; - } + public static final String TimerBeepPreferenceKey = "preference_timer_beep"; - public static String getTimerSpeakPreferenceKey() { - return "preference_timer_speak"; - } + public static final String TimerSpeakPreferenceKey = "preference_timer_speak"; - public static String getRepeatModePreferenceKey() { - // note for historical reasons the preference refers to burst; the feature was renamed to - // "repeat" in v1.43, but we still need to use the old string to avoid changing user settings - // when people upgrade - return "preference_burst_mode"; - } + // note for historical reasons the preference refers to burst; the feature was renamed to + // "repeat" in v1.43, but we still need to use the old string to avoid changing user settings + // when people upgrade + public static final String RepeatModePreferenceKey = "preference_burst_mode"; - public static String getRepeatIntervalPreferenceKey() { - // see note about "repeat" vs "burst" under getRepeatModePreferenceKey() - return "preference_burst_interval"; - } + // see note about "repeat" vs "burst" under RepeatModePreferenceKey + public static final String RepeatIntervalPreferenceKey = "preference_burst_interval"; - public static String getShutterSoundPreferenceKey() { - return "preference_shutter_sound"; - } + public static final String ShutterSoundPreferenceKey = "preference_shutter_sound"; public static final String ImmersiveModePreferenceKey = "preference_immersive_mode"; + public static final String AddYPRToComments="preference_comment_ypr"; } diff --git a/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java b/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java index 142396cb0cf5d3e38560561d2331aec154ff634f..22b10d5faad18a49bd018c81ddc2cd5affb097a7 100644 --- a/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java +++ b/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java @@ -111,10 +111,10 @@ public class SettingsManager { switch( name ) { case boolean_tag: - editor.putBoolean(key, Boolean.valueOf(parser.getAttributeValue(null, "value"))); + editor.putBoolean(key, Boolean.parseBoolean(parser.getAttributeValue(null, "value"))); break; case float_tag: - editor.putFloat(key, Float.valueOf(parser.getAttributeValue(null, "value"))); + editor.putFloat(key, Float.parseFloat(parser.getAttributeValue(null, "value"))); break; case int_tag: editor.putInt(key, Integer.parseInt(parser.getAttributeValue(null, "value"))); diff --git a/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java b/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java index 341257e918735d7d8e375a8cafa766488f085709..adf0346af72812c9fcbb9ee46282def12eb3591d 100644 --- a/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java +++ b/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java @@ -95,6 +95,7 @@ class SpeechControl { if( !speechRecognizerIsStarted ) { if( MyDebug.LOG ) Log.d(TAG, "...but speech recognition already stopped"); + //noinspection UnnecessaryReturnStatement return; } } @@ -106,6 +107,7 @@ class SpeechControl { if( !speechRecognizerIsStarted ) { if( MyDebug.LOG ) Log.d(TAG, "...but speech recognition already stopped"); + //noinspection UnnecessaryReturnStatement return; } } @@ -163,6 +165,7 @@ class SpeechControl { if( !speechRecognizerIsStarted ) { if( MyDebug.LOG ) Log.d(TAG, "...but speech recognition already stopped"); + //noinspection UnnecessaryReturnStatement return; } } @@ -174,6 +177,7 @@ class SpeechControl { if( !speechRecognizerIsStarted ) { if( MyDebug.LOG ) Log.d(TAG, "...but speech recognition already stopped"); + //noinspection UnnecessaryReturnStatement return; } } @@ -185,6 +189,7 @@ class SpeechControl { if( !speechRecognizerIsStarted ) { if( MyDebug.LOG ) Log.d(TAG, "...but speech recognition already stopped"); + //noinspection UnnecessaryReturnStatement return; } } diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 5f5ec835f2924812193c51f623ae93f596a35a67..70529cac2387f51e6cffb96ae9195de6466a94c3 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -292,7 +292,7 @@ public class StorageUtils { // check Android version just to be safe if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - if( sharedPreferences.getBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), false) ) { + if( sharedPreferences.getBoolean(PreferenceKeys.UsingSAFPreferenceKey, false) ) { return true; } } @@ -302,13 +302,13 @@ public class StorageUtils { // only valid if !isUsingSAF() String getSaveLocation() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - return sharedPreferences.getString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCamera"); + return sharedPreferences.getString(PreferenceKeys.SaveLocationPreferenceKey, "OpenCamera"); } // only valid if isUsingSAF() String getSaveLocationSAF() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - return sharedPreferences.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), ""); + return sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, ""); } // only valid if isUsingSAF() @@ -367,7 +367,7 @@ public class StorageUtils { * See: http://stackoverflow.com/questions/21605493/storage-access-framework-does-not-update-mediascanner-mtp http://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework/ - Also note that this will always return null with Android Q's scoped storage: https://developer.android.com/preview/privacy/scoped-storage + Also note that this will return null for media store Uris with Android Q's scoped storage: https://developer.android.com/preview/privacy/scoped-storage "The DATA column is redacted for each file in the media store." */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -390,9 +390,9 @@ public class StorageUtils { if( MyDebug.LOG ) Log.d(TAG, "id: " + id); String [] split = id.split(":"); - if( split.length >= 2 ) { + if( split.length >= 1 ) { String type = split[0]; - String path = split[1]; + String path = split.length >= 2 ? split[1] : ""; /*if( MyDebug.LOG ) { Log.d(TAG, "type: " + type); Log.d(TAG, "path: " + path); @@ -520,14 +520,16 @@ public class StorageUtils { * See https://developer.android.com/guide/topics/providers/document-provider.html and * http://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content . */ - public String getFileName(Uri uri) { + String getFileName(Uri uri) { if( MyDebug.LOG ) { Log.d(TAG, "getFileName: " + uri); Log.d(TAG, "uri has path: " + uri.getPath()); } String result = null; if( uri.getScheme() != null && uri.getScheme().equals("content") ) { - try( Cursor cursor = context.getContentResolver().query(uri, null, null, null, null) ) { + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(uri, null, null, null, null); if( cursor != null && cursor.moveToFirst() ) { final int column_index = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME); result = cursor.getString(column_index); @@ -540,6 +542,10 @@ public class StorageUtils { Log.e(TAG, "Exception trying to find filename"); e.printStackTrace(); } + finally { + if (cursor != null) + cursor.close(); + } } if( result == null ) { if( MyDebug.LOG ) @@ -561,7 +567,7 @@ public class StorageUtils { index = "_" + count; // try to find a unique filename } SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean useZuluTime = sharedPreferences.getString(PreferenceKeys.getSaveZuluTimePreferenceKey(), "local").equals("zulu"); + boolean useZuluTime = sharedPreferences.getString(PreferenceKeys.SaveZuluTimePreferenceKey, "local").equals("zulu"); String timeStamp; if( useZuluTime ) { SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd_HHmmss'Z'", Locale.US); @@ -575,12 +581,12 @@ public class StorageUtils { switch (type) { case MEDIA_TYPE_GYRO_INFO: // gyro info files have same name as the photo (but different extension) case MEDIA_TYPE_IMAGE: { - String prefix = sharedPreferences.getString(PreferenceKeys.getSavePhotoPrefixPreferenceKey(), "IMG_"); + String prefix = sharedPreferences.getString(PreferenceKeys.SavePhotoPrefixPreferenceKey, "IMG_"); mediaFilename = prefix + timeStamp + suffix + index + extension; break; } case MEDIA_TYPE_VIDEO: { - String prefix = sharedPreferences.getString(PreferenceKeys.getSaveVideoPrefixPreferenceKey(), "VID_"); + String prefix = sharedPreferences.getString(PreferenceKeys.SaveVideoPrefixPreferenceKey, "VID_"); mediaFilename = prefix + timeStamp + suffix + index + extension; break; } @@ -744,8 +750,6 @@ public class StorageUtils { } break; case MEDIA_TYPE_PREFS: - mimeType = "text/xml"; - break; case MEDIA_TYPE_GYRO_INFO: mimeType = "text/xml"; break; @@ -766,52 +770,80 @@ public class StorageUtils { final Uri uri; final long date; final int orientation; - final String path; + final String filename; // this should correspond to DISPLAY_NAME (so available with scoped storage) - so this includes file extension, but not full path - Media(long id, boolean video, Uri uri, long date, int orientation, String path) { + Media(long id, boolean video, Uri uri, long date, int orientation, String filename) { this.id = id; this.video = video; this.uri = uri; this.date = date; this.orientation = orientation; - this.path = path; + this.filename = filename; } } - private Media getLatestMedia(boolean video) { - if( MyDebug.LOG ) - Log.d(TAG, "getLatestMedia: " + (video ? "video" : "images")); - if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ) { - // needed for Android 6, in case users deny storage permission, otherwise we get java.lang.SecurityException from ContentResolver.query() - // see https://developer.android.com/training/permissions/requesting.html - // we now request storage permission before opening the camera, but keep this here just in case - // we restrict check to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener() - if( MyDebug.LOG ) - Log.e(TAG, "don't have READ_EXTERNAL_STORAGE permission"); - return null; + private Media getLatestMediaCore(Uri baseUri, String bucket_id, boolean video) { + if( MyDebug.LOG ) { + Log.d(TAG, "getLatestMediaCore"); + Log.d(TAG, "baseUri: " + baseUri); + Log.d(TAG, "bucket_id: " + bucket_id); + Log.d(TAG, "video: " + video); } Media media = null; - Uri baseUri = video ? Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + final int column_id_c = 0; final int column_date_taken_c = 1; - final int column_data_c = 2; // full path and filename, including extension + /*final int column_data_c = 2; // full path and filename, including extension + final int column_name_c = 3; // filename (without path), including extension + final int column_orientation_c = 4; // for images only*/ + final int column_name_c = 2; // filename (without path), including extension final int column_orientation_c = 3; // for images only - String [] projection = video ? new String[] {VideoColumns._ID, VideoColumns.DATE_TAKEN, VideoColumns.DATA} : new String[] {ImageColumns._ID, ImageColumns.DATE_TAKEN, ImageColumns.DATA, ImageColumns.ORIENTATION}; + String [] projection = video ? + new String[] {VideoColumns._ID, VideoColumns.DATE_TAKEN, VideoColumns.DISPLAY_NAME} : + new String[] {ImageColumns._ID, ImageColumns.DATE_TAKEN, ImageColumns.DISPLAY_NAME, ImageColumns.ORIENTATION}; // for images, we need to search for JPEG/etc and RAW, to support RAW only mode (even if we're not currently in that mode, it may be that previously the user did take photos in RAW only mode) - String selection = video ? "" : ImageColumns.MIME_TYPE + "='image/jpeg' OR " + + /*String selection = video ? "" : ImageColumns.MIME_TYPE + "='image/jpeg' OR " + ImageColumns.MIME_TYPE + "='image/webp' OR " + ImageColumns.MIME_TYPE + "='image/png' OR " + - ImageColumns.MIME_TYPE + "='image/x-adobe-dng'"; + ImageColumns.MIME_TYPE + "='image/x-adobe-dng'";*/ + String selection = ""; + if( bucket_id != null ) + selection = (video ? VideoColumns.BUCKET_ID : ImageColumns.BUCKET_ID) + " = " + bucket_id; + if( !video ) { + boolean and = selection.length() > 0; + if( and ) + selection += " AND ( "; + selection += ImageColumns.MIME_TYPE + "='image/jpeg' OR " + + ImageColumns.MIME_TYPE + "='image/webp' OR " + + ImageColumns.MIME_TYPE + "='image/png' OR " + + ImageColumns.MIME_TYPE + "='image/x-adobe-dng'"; + if( and ) + selection += " )"; + } + if( MyDebug.LOG ) + Log.d(TAG, "selection: " + selection); String order = video ? VideoColumns.DATE_TAKEN + " DESC," + VideoColumns._ID + " DESC" : ImageColumns.DATE_TAKEN + " DESC," + ImageColumns._ID + " DESC"; Cursor cursor = null; + + // we know we only want the most recent image - however we may need to scan forward if we find a RAW, to see if there's + // an equivalent non-RAW image + // request 3, just in case + Uri queryUri = baseUri.buildUpon().appendQueryParameter("limit", "3").build(); + if( MyDebug.LOG ) + Log.d(TAG, "queryUri: " + queryUri); + try { - cursor = context.getContentResolver().query(baseUri, projection, selection, null, order); + cursor = context.getContentResolver().query(queryUri, projection, selection, null, order); if( cursor != null && cursor.moveToFirst() ) { if( MyDebug.LOG ) Log.d(TAG, "found: " + cursor.getCount()); + + // now sorted in order of date - so just pick the most recent one + + /* // now sorted in order of date - scan to most recent one in the Open Camera save folder boolean found = false; - File save_folder = getImageFolder(); // may be null if using SAF + //File save_folder = getImageFolder(); // may be null if using SAF String save_folder_string = save_folder == null ? null : save_folder.getAbsolutePath() + File.separator; if( MyDebug.LOG ) Log.d(TAG, "save_folder_string: " + save_folder_string); @@ -838,50 +870,66 @@ public class StorageUtils { } } while( cursor.moveToNext() ); - if( found ) { + + if( !found ) { + if( MyDebug.LOG ) + Log.d(TAG, "can't find suitable in Open Camera folder, so just go with most recent"); + cursor.moveToFirst(); + } + */ + + { // make sure we prefer JPEG/etc (non RAW) if there's a JPEG/etc version of this image // this is because we want to support RAW only and JPEG+RAW modes - String path = cursor.getString(column_data_c); - if( MyDebug.LOG ) - Log.d(TAG, "path: " + path); - // path may be null on Android 4.4, see above! - if( path != null && path.toLowerCase(Locale.US).endsWith(".dng") ) { + String filename = cursor.getString(column_name_c); + if( MyDebug.LOG ) { + Log.d(TAG, "filename: " + filename); + } + // in theory now that we use DISPLAY_NAME instead of DATA (for path), this should always be non-null, but check just in case + if( filename != null && filename.toLowerCase(Locale.US).endsWith(".dng") ) { if( MyDebug.LOG ) Log.d(TAG, "try to find a non-RAW version of the DNG"); int dng_pos = cursor.getPosition(); boolean found_non_raw = false; - String path_without_ext = path.toLowerCase(Locale.US); - if( path_without_ext.indexOf(".") > 0 ) - path_without_ext = path_without_ext.substring(0, path_without_ext.lastIndexOf(".")); + String filename_without_ext = filename.toLowerCase(Locale.US); + if( filename_without_ext.indexOf(".") > 0 ) + filename_without_ext = filename_without_ext.substring(0, filename_without_ext.lastIndexOf(".")); if( MyDebug.LOG ) - Log.d(TAG, "path_without_ext: " + path_without_ext); + Log.d(TAG, "filename_without_ext: " + filename_without_ext); while( cursor.moveToNext() ) { - String next_path = cursor.getString(column_data_c); + String next_filename = cursor.getString(column_name_c); if( MyDebug.LOG ) - Log.d(TAG, "next_path: " + next_path); - if( next_path == null ) + Log.d(TAG, "next_filename: " + next_filename); + if( next_filename == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "done scanning, couldn't find filename"); break; - String next_path_without_ext = next_path.toLowerCase(Locale.US); - if( next_path_without_ext.indexOf(".") > 0 ) - next_path_without_ext = next_path_without_ext.substring(0, next_path_without_ext.lastIndexOf(".")); + } + String next_filename_without_ext = next_filename.toLowerCase(Locale.US); + if( next_filename_without_ext.indexOf(".") > 0 ) + next_filename_without_ext = next_filename_without_ext.substring(0, next_filename_without_ext.lastIndexOf(".")); if( MyDebug.LOG ) - Log.d(TAG, "next_path_without_ext: " + next_path_without_ext); - if( !path_without_ext.equals(next_path_without_ext) ) + Log.d(TAG, "next_filename_without_ext: " + next_filename_without_ext); + if( !filename_without_ext.equals(next_filename_without_ext) ) { + // no point scanning any further as sorted by date - and we don't want to read through the entire set! + if( MyDebug.LOG ) + Log.d(TAG, "done scanning"); break; + } // so we've found another file with matching filename - is it a JPEG/etc? - if( next_path.toLowerCase(Locale.US).endsWith(".jpg") ) { + if( next_filename.toLowerCase(Locale.US).endsWith(".jpg") ) { if( MyDebug.LOG ) Log.d(TAG, "found equivalent jpeg"); found_non_raw = true; break; } - else if( next_path.toLowerCase(Locale.US).endsWith(".webp") ) { + else if( next_filename.toLowerCase(Locale.US).endsWith(".webp") ) { if( MyDebug.LOG ) Log.d(TAG, "found equivalent webp"); found_non_raw = true; break; } - else if( next_path.toLowerCase(Locale.US).endsWith(".png") ) { + else if( next_filename.toLowerCase(Locale.US).endsWith(".png") ) { if( MyDebug.LOG ) Log.d(TAG, "found equivalent png"); found_non_raw = true; @@ -895,19 +943,19 @@ public class StorageUtils { } } } - if( !found ) { - if( MyDebug.LOG ) - Log.d(TAG, "can't find suitable in Open Camera folder, so just go with most recent"); - cursor.moveToFirst(); - } + long id = cursor.getLong(column_id_c); long date = cursor.getLong(column_date_taken_c); int orientation = video ? 0 : cursor.getInt(column_orientation_c); Uri uri = ContentUris.withAppendedId(baseUri, id); - String path = cursor.getString(column_data_c); + String filename = cursor.getString(column_name_c); if( MyDebug.LOG ) Log.d(TAG, "found most recent uri for " + (video ? "video" : "images") + ": " + uri); - media = new Media(id, video, uri, date, orientation, path); + media = new Media(id, video, uri, date, orientation, filename); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "mediastore returned no media"); } } catch(Exception e) { @@ -921,6 +969,41 @@ public class StorageUtils { cursor.close(); } } + + return media; + } + + private Media getLatestMedia(boolean video) { + if( MyDebug.LOG ) + Log.d(TAG, "getLatestMedia: " + (video ? "video" : "images")); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ) { + // needed for Android 6, in case users deny storage permission, otherwise we get java.lang.SecurityException from ContentResolver.query() + // see https://developer.android.com/training/permissions/requesting.html + // we now request storage permission before opening the camera, but keep this here just in case + // we restrict check to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener() + if( MyDebug.LOG ) + Log.e(TAG, "don't have READ_EXTERNAL_STORAGE permission"); + return null; + } + + File save_folder = getImageFolder(); // may be null if using SAF + if( MyDebug.LOG ) + Log.d(TAG, "save_folder: " + save_folder); + String bucket_id = null; + if( save_folder != null ) { + bucket_id = String.valueOf(save_folder.getAbsolutePath().toLowerCase().hashCode()); + } + if( MyDebug.LOG ) + Log.d(TAG, "bucket_id: " + bucket_id); + + Uri baseUri = video ? Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + Media media = getLatestMediaCore(baseUri, bucket_id, video); + if( media == null && bucket_id != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "fall back to checking any folder"); + media = getLatestMediaCore(baseUri, null, video); + } + return media; } diff --git a/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java b/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java index d2b942a34d2915dc02980d596b6987c959e1f9dc..a80f5a1ad17a37212ac87cd8d360bc6833ce2628 100644 --- a/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java +++ b/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java @@ -11,7 +11,11 @@ import android.util.Log; */ public class TakePhoto extends Activity { private static final String TAG = "TakePhoto"; - public static final String TAKE_PHOTO = "net.sourceforge.opencamera.TAKE_PHOTO"; + + // Usually passing data via intent is preferred to using statics - however here a static is better for security, + // as we don't want other applications calling Open Camera's MainActivity with a take photo intent! + //public static final String TAKE_PHOTO = "net.sourceforge.opencamera.TAKE_PHOTO"; + public static boolean TAKE_PHOTO; @Override protected void onCreate(Bundle savedInstanceState) { @@ -21,7 +25,8 @@ public class TakePhoto extends Activity { Intent intent = new Intent(this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.putExtra(TAKE_PHOTO, true); + //intent.putExtra(TAKE_PHOTO, true); + TakePhoto.TAKE_PHOTO = true; this.startActivity(intent); if( MyDebug.LOG ) Log.d(TAG, "finish"); diff --git a/app/src/main/java/net/sourceforge/opencamera/TextFormatter.java b/app/src/main/java/net/sourceforge/opencamera/TextFormatter.java index b3f73416f54e54e013fd94b14a6b2ad2708b49c2..897002c6c326ba62b3ce3f87bb3ec01cf08b43b4 100644 --- a/app/src/main/java/net/sourceforge/opencamera/TextFormatter.java +++ b/app/src/main/java/net/sourceforge/opencamera/TextFormatter.java @@ -110,8 +110,7 @@ public class TextFormatter { gps_stamp += "" + Math.round(geo_angle) + (char)0x00B0; } } - if( MyDebug.LOG ) - Log.d(TAG, "gps_stamp: " + gps_stamp); + // don't log gps_stamp, in case of privacy! return gps_stamp; } diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java index 8fa1dcf57769406aa92dcde252a95c005937cb6d..49835b9b83db241f0a033ac754e3687eb588b92d 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java @@ -38,6 +38,7 @@ public abstract class CameraController { public static final String ISO_DEFAULT = "auto"; public static final long EXPOSURE_TIME_DEFAULT = 1000000000L/30; // note, responsibility of callers to check that this is within the valid min/max range + public static final int ISO_FOR_DARK = 1100; public static final int N_IMAGES_NR_DARK = 8; public static final int N_IMAGES_NR_DARK_LOW_LIGHT = 15; @@ -52,6 +53,8 @@ public abstract class CameraController { public volatile int test_fake_flash_photo; // for Camera2 API, records torch turning on for fake flash for photo capture public volatile int test_af_state_null_focus; // for Camera2 API, records af_state being null even when we've requested autofocus public volatile boolean test_used_tonemap_curve; + public volatile int test_texture_view_buffer_w; // for TextureView, keep track of buffer size + public volatile int test_texture_view_buffer_h; public static class CameraFeatures { public boolean is_zoom_supported; @@ -64,10 +67,12 @@ public abstract class CameraController { public List preview_sizes; public List supported_flash_values; public List supported_focus_values; + public float [] apertures; // may be null if not supported, else will have at least 2 values public int max_num_focus_areas; public float minimum_focus_distance; public boolean is_exposure_lock_supported; public boolean is_white_balance_lock_supported; + public boolean is_optical_stabilization_supported; public boolean is_video_stabilization_supported; public boolean is_photo_video_recording_supported; public boolean supports_white_balance_temperature; @@ -358,6 +363,7 @@ public abstract class CameraController { public abstract int getISO(); public abstract long getExposureTime(); public abstract boolean setExposureTime(long exposure_time); + public abstract void setAperture(float aperture); public abstract CameraController.Size getPictureSize(); public abstract void setPictureSize(int width, int height); public abstract CameraController.Size getPreviewSize(); @@ -420,6 +426,10 @@ public abstract class CameraController { * caught by the callera). */ public abstract void setRaw(boolean want_raw, int max_raw_images); + + /** Request a capture session compatible with high speed frame rates. + * This should be called only when the preview is paused or not yet started. + */ public abstract void setVideoHighSpeed(boolean setVideoHighSpeed); /** * setUseCamera2FakeFlash() should be called after creating the CameraController, and before calling getCameraFeatures() or @@ -440,10 +450,30 @@ public abstract class CameraController { public boolean getUseCamera2FakeFlash() { return false; } + public abstract boolean getOpticalStabilization(); + /** Whether to enable digital video stabilization. Should only be set to true when intending to + * capture video. + */ public abstract void setVideoStabilization(boolean enabled); public abstract boolean getVideoStabilization(); - public abstract void setLogProfile(boolean use_log_profile, float log_profile_strength); - public abstract boolean isLogProfile(); + public enum TonemapProfile { + TONEMAPPROFILE_OFF, + TONEMAPPROFILE_REC709, + TONEMAPPROFILE_SRGB, + TONEMAPPROFILE_LOG, + TONEMAPPROFILE_GAMMA, + TONEMAPPROFILE_JTVIDEO, + TONEMAPPROFILE_JTLOG, + TONEMAPPROFILE_JTLOG2 + } + + /** Sets a tonemap profile. + * @param tonemap_profile The type of the tonemap profile. + * @param log_profile_strength Only relevant if tonemap_profile set to TONEMAPPROFILE_LOG. + * @param gamma Only relevant if tonemap_profile set to TONEMAPPROFILE_GAMMA + */ + public abstract void setTonemapProfile(TonemapProfile tonemap_profile, float log_profile_strength, float gamma); + public abstract TonemapProfile getTonemapProfile(); public abstract int getJpegQuality(); public abstract void setJpegQuality(int quality); public abstract int getZoom(); @@ -494,6 +524,12 @@ public abstract class CameraController { public abstract void reconnect() throws CameraControllerException; public abstract void setPreviewDisplay(SurfaceHolder holder) throws CameraControllerException; public abstract void setPreviewTexture(TextureView texture) throws CameraControllerException; + /** This should be called when using a TextureView, and the texture view has reported a change + * in size via onSurfaceTextureSizeChanged. + */ + public void updatePreviewTexture() { + // dummy implementation + } /** Starts the camera preview. * @throws CameraControllerException if the camera preview fails to start. */ @@ -518,7 +554,15 @@ public abstract class CameraController { public abstract void setDisplayOrientation(int degrees); public abstract int getDisplayOrientation(); public abstract int getCameraOrientation(); - public abstract boolean isFrontFacing(); + public enum Facing { + FACING_BACK, + FACING_FRONT, + FACING_EXTERNAL, + FACING_UNKNOWN // returned if the Camera API returned an error or an unknown type + } + /** Returns whether the camera is front, back or external. + */ + public abstract Facing getFacing(); public abstract void unlock(); /** Call to initialise video recording, should call before MediaRecorder.prepare(). * @param video_recorder The media recorder object. @@ -568,6 +612,12 @@ public abstract class CameraController { } public long captureResultFrameDuration() { return 0; + } + public boolean captureResultHasAperture() { + return false; + } + public float captureResultAperture() { + return 0.0f; } /*public boolean captureResultHasFocusDistance() { return false; diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java index 5c8cb8eb4214b7e5c108744795ec6fc34e7709d7..1310a17ef31369d9d45dc8dc344e058b5ecc2f95 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java @@ -208,7 +208,7 @@ public class CameraController1 extends CameraController { Log.d(TAG, "flash supported"); } else { - if( isFrontFacing() ) { + if( getFacing() == Facing.FACING_FRONT ) { if( MyDebug.LOG ) Log.d(TAG, "front-screen with no flash"); output_modes.clear(); // clear any pre-existing mode (see note above about Samsung Galaxy S7) @@ -717,6 +717,11 @@ public class CameraController1 extends CameraController { return false; } + @Override + public void setAperture(float aperture) { + // not supported for CameraController1 + } + @Override public CameraController.Size getPictureSize() { /*Camera.Parameters parameters = this.getParameters(); @@ -871,6 +876,12 @@ public class CameraController1 extends CameraController { // not supported for CameraController1 } + @Override + public boolean getOpticalStabilization() { + // not supported for CameraController1 + return false; + } + @Override public void setVideoStabilization(boolean enabled) { Camera.Parameters parameters = this.getParameters(); @@ -884,14 +895,14 @@ public class CameraController1 extends CameraController { } @Override - public void setLogProfile(boolean use_log_profile, float log_profile_strength) { + public void setTonemapProfile(TonemapProfile tonemap_profile, float log_profile_strength, float gamma) { // not supported for CameraController1! } @Override - public boolean isLogProfile() { + public TonemapProfile getTonemapProfile() { // not supported for CameraController1! - return false; + return TonemapProfile.TONEMAPPROFILE_OFF; } public int getJpegQuality() { @@ -1128,6 +1139,8 @@ public class CameraController1 extends CameraController { String flash_mode = ""; switch(flash_value) { case "flash_off": + case "flash_frontscreen_on": + case "flash_frontscreen_torch": flash_mode = Camera.Parameters.FLASH_MODE_OFF; break; case "flash_auto": @@ -1142,10 +1155,6 @@ public class CameraController1 extends CameraController { case "flash_red_eye": flash_mode = Camera.Parameters.FLASH_MODE_RED_EYE; break; - case "flash_frontscreen_on": - case "flash_frontscreen_torch": - flash_mode = Camera.Parameters.FLASH_MODE_OFF; - break; } return flash_mode; } @@ -1297,6 +1306,9 @@ public class CameraController1 extends CameraController { } public void setLocationInfo(Location location) { + // don't log location, in case of privacy! + if( MyDebug.LOG ) + Log.d(TAG, "setLocationInfo"); Camera.Parameters parameters = this.getParameters(); parameters.removeGpsData(); parameters.setGpsTimestamp(System.currentTimeMillis() / 1000); // initialise to a value (from Android camera source) @@ -1868,8 +1880,16 @@ public class CameraController1 extends CameraController { return camera_info.orientation; } - public boolean isFrontFacing() { - return (camera_info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT); + @Override + public Facing getFacing() { + switch( camera_info.facing ) { + case Camera.CameraInfo.CAMERA_FACING_FRONT: + return Facing.FACING_FRONT; + case Camera.CameraInfo.CAMERA_FACING_BACK: + return Facing.FACING_BACK; + } + Log.e(TAG, "unknown camera_facing: " + camera_info.facing); + return Facing.FACING_UNKNOWN; } public void unlock() { @@ -1883,7 +1903,7 @@ public class CameraController1 extends CameraController { } @Override - public void initVideoRecorderPostPrepare(MediaRecorder video_recorder, boolean want_photo_video_recording) throws CameraControllerException { + public void initVideoRecorderPostPrepare(MediaRecorder video_recorder, boolean want_photo_video_recording) { // no further actions necessary } diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 3e6d83b83bd9c1fa6c5fd02248284f6308b3a60e..d7a943605409c8dd3b4da403b11b8153abd80d6c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -11,7 +11,6 @@ import java.util.List; import java.util.Locale; import java.util.Queue; -import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.ImageFormat; @@ -45,7 +44,9 @@ import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; import android.util.Log; +import android.util.Pair; import android.util.Range; import android.util.SizeF; import android.view.Display; @@ -56,7 +57,7 @@ import android.view.TextureView; /** Provides support using Android 5's Camera 2 API * android.hardware.camera2.*. */ -@TargetApi(Build.VERSION_CODES.LOLLIPOP) +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class CameraController2 extends CameraController { private static final String TAG = "CameraController2"; @@ -64,19 +65,85 @@ public class CameraController2 extends CameraController { private CameraDevice camera; private String cameraIdS; + private final boolean is_samsung; private final boolean is_samsung_s7; // Galaxy S7 or Galaxy S7 Edge private CameraCharacteristics characteristics; // cached characteristics (use this for values that need to be frequently accessed, e.g., per frame, to improve performance); private int characteristics_sensor_orientation; - private boolean characteristics_is_front_facing; + private Facing characteristics_facing; private List zoom_ratios; private int current_zoom_value; private boolean supports_face_detect_mode_simple; private boolean supports_face_detect_mode_full; + private boolean supports_optical_stabilization; private boolean supports_photo_video_recording; - private final static int tonemap_max_curve_points_c = 64; + private boolean supports_white_balance_temperature; + + private final static int tonemap_log_max_curve_points_c = 64; + private final static float [] jtvideo_values_base = new float[] { + 0.00f, 0.00f, + 0.01f, 0.055f, + 0.02f, 0.1f, + 0.05f, 0.21f, + 0.09f, 0.31f, + 0.13f, 0.38f, + 0.18f, 0.45f, + 0.28f, 0.57f, + 0.35f, 0.64f, + 0.45f, 0.72f, + 0.51f, 0.76f, + 0.60f, 0.82f, + 0.67f, 0.86f, + 0.77f, 0.91f, + 0.88f, 0.96f, + 0.97f, 0.99f, + 1.00f, 1.00f + }; + private final float [] jtvideo_values; + private final static float [] jtlog_values_base = new float[] { + 0.00f, 0.00f, + 0.01f, 0.07f, + 0.03f, 0.17f, + 0.05f, 0.25f, + 0.07f, 0.31f, + 0.09f, 0.36f, + 0.13f, 0.44f, + 0.18f, 0.51f, + 0.24f, 0.57f, + 0.31f, 0.64f, + 0.38f, 0.70f, + 0.46f, 0.76f, + 0.58f, 0.83f, + 0.70f, 0.89f, + 0.86f, 0.95f, + 0.99f, 0.99f, + 1.00f, 1.00f + }; + private final float [] jtlog_values; + private final static float [] jtlog2_values_base = new float[] { + 0.00f, 0.00f, + 0.01f, 0.09f, + 0.03f, 0.23f, + 0.07f, 0.37f, + 0.12f, 0.48f, + 0.17f, 0.56f, + 0.25f, 0.64f, + 0.32f, 0.70f, + 0.39f, 0.75f, + 0.50f, 0.81f, + 0.59f, 0.85f, + 0.66f, 0.88f, + 0.72f, 0.9f, + 0.78f, 0.92f, + 0.88f, 0.95f, + 0.92f, 0.96f, + 0.99f, 0.98f, + 1.00f, 1.00f + }; + private final float [] jtlog2_values; + private final ErrorCallback preview_error_cb; private final ErrorCallback camera_error_cb; private CameraCaptureSession captureSession; @@ -190,6 +257,8 @@ public class CameraController2 extends CameraController { private long capture_result_exposure_time; private boolean capture_result_has_frame_duration; private long capture_result_frame_duration; + private boolean capture_result_has_aperture; + private float capture_result_aperture; /*private boolean capture_result_has_focus_distance; private float capture_result_focus_distance_min; private float capture_result_focus_distance_max;*/ @@ -197,8 +266,8 @@ public class CameraController2 extends CameraController { private enum RequestTagType { CAPTURE, // request is either for a regular non-burst capture, or the last of a burst capture sequence - CAPTURE_BURST_IN_PROGRESS, // request is for a burst capture, but isn't the last of the burst capture sequence - NONE // should be treated the same as if no tag had been set on the request - but allows the request tag type to be changed later + CAPTURE_BURST_IN_PROGRESS // request is for a burst capture, but isn't the last of the burst capture sequence + //NONE // should be treated the same as if no tag had been set on the request - but allows the request tag type to be changed later } /* The class that we use for setTag() and getTag() for capture requests. @@ -249,6 +318,8 @@ public class CameraController2 extends CameraController { //private int flash_mode = CameraMetadata.FLASH_MODE_OFF; private int iso; private long exposure_time = EXPOSURE_TIME_DEFAULT; + private boolean has_aperture; + private float aperture; private Rect scalar_crop_region; // no need for has_scalar_crop_region, as we can set to null instead private boolean has_ae_exposure_compensation; private int ae_exposure_compensation; @@ -262,9 +333,11 @@ public class CameraController2 extends CameraController { private MeteringRectangle [] ae_regions; // no need for has_scalar_crop_region, as we can set to null instead private boolean has_face_detect_mode; private int face_detect_mode = CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF; + private Integer default_optical_stabilization; private boolean video_stabilization; - private boolean use_log_profile; - private float log_profile_strength; + private TonemapProfile tonemap_profile = TonemapProfile.TONEMAPPROFILE_OFF; + private float log_profile_strength; // for TONEMAPPROFILE_LOG + private float gamma_profile; // for TONEMAPPROFILE_GAMMA private Integer default_tonemap_mode; // since we don't know what a device's tonemap mode is, we save it so we can switch back to it private Range ae_target_fps_range; private long sensor_frame_duration; @@ -276,7 +349,7 @@ public class CameraController2 extends CameraController { exif_orientation = ExifInterface.ORIENTATION_NORMAL; break; case 90: - exif_orientation = isFrontFacing() ? + exif_orientation = (getFacing() == Facing.FACING_FRONT) ? ExifInterface.ORIENTATION_ROTATE_270 : ExifInterface.ORIENTATION_ROTATE_90; break; @@ -284,7 +357,7 @@ public class CameraController2 extends CameraController { exif_orientation = ExifInterface.ORIENTATION_ROTATE_180; break; case 270: - exif_orientation = isFrontFacing() ? + exif_orientation = (getFacing() == Facing.FACING_FRONT) ? ExifInterface.ORIENTATION_ROTATE_90 : ExifInterface.ORIENTATION_ROTATE_270; break; @@ -325,8 +398,8 @@ public class CameraController2 extends CameraController { setAERegions(builder); setFaceDetectMode(builder); setRawMode(builder); - setVideoStabilization(builder); - setLogProfile(builder); + setStabilization(builder); + setTonemapProfile(builder); if( is_still ) { if( location != null ) { @@ -394,6 +467,8 @@ public class CameraController2 extends CameraController { Log.d(TAG, "raw_sensitivity_boost: " + (raw_sensitivity_boost==null ? "null" : raw_sensitivity_boost)); }*/ } + //Integer ois_mode = builder.get(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE); + //Log.d(TAG, "ois_mode: " + (ois_mode==null ? "null" : ois_mode)); } } @@ -559,6 +634,20 @@ public class CameraController2 extends CameraController { return changed; } + private boolean setAperture(CaptureRequest.Builder builder) { + if( MyDebug.LOG ) + Log.d(TAG, "setAperture"); + // don't set at all if has_aperture==false + if( has_aperture ) { + if( MyDebug.LOG ) + Log.d(TAG, " aperture: " + aperture); + builder.set(CaptureRequest.LENS_APERTURE, aperture); + return true; + } + return false; + } + + @SuppressWarnings("SameReturnValue") private boolean setAEMode(CaptureRequest.Builder builder, boolean is_still) { if( MyDebug.LOG ) Log.d(TAG, "setAEMode"); @@ -583,24 +672,13 @@ public class CameraController2 extends CameraController { } //builder.set(CaptureRequest.SENSOR_FRAME_DURATION, 1000000000L); //builder.set(CaptureRequest.SENSOR_FRAME_DURATION, 0L); - // for now, flash is disabled when using manual iso - it seems to cause ISO level to jump to 100 on Nexus 6 when flash is turned on! - builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); - // set flash via CaptureRequest.FLASH - /*if( flash_value.equals("flash_off") ) { - builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); - } - else if( flash_value.equals("flash_auto") ) { - builder.set(CaptureRequest.FLASH_MODE, is_still ? CameraMetadata.FLASH_MODE_SINGLE : CameraMetadata.FLASH_MODE_OFF); - } - else if( flash_value.equals("flash_on") ) { - builder.set(CaptureRequest.FLASH_MODE, is_still ? CameraMetadata.FLASH_MODE_SINGLE : CameraMetadata.FLASH_MODE_OFF); - } - else if( flash_value.equals("flash_torch") ) { + // only need to account for FLASH_MODE_TORCH, otherwise we use fake flash mode for manual ISO + if( flash_value.equals("flash_torch") ) { builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); } - else if( flash_value.equals("flash_red_eye") ) { - builder.set(CaptureRequest.FLASH_MODE, is_still ? CameraMetadata.FLASH_MODE_SINGLE : CameraMetadata.FLASH_MODE_OFF); - }*/ + else { + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + } } else { if( MyDebug.LOG ) { @@ -613,6 +691,7 @@ public class CameraController2 extends CameraController { } // prefer to set flash via the ae mode (otherwise get even worse results), except for torch which we can't + //noinspection DuplicateBranchesInSwitch switch(flash_value) { case "flash_off": builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); @@ -731,13 +810,34 @@ public class CameraController2 extends CameraController { } } - private void setVideoStabilization(CaptureRequest.Builder builder) { + private void setStabilization(CaptureRequest.Builder builder) { + if( MyDebug.LOG ) + Log.d(TAG, "setStabilization: " + video_stabilization); builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, video_stabilization ? CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON : CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF); + if( supports_optical_stabilization ) { + if( video_stabilization ) { + // should also disable OIS + if( default_optical_stabilization == null ) { + // save the default optical_stabilization + default_optical_stabilization = builder.get(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE); + if( MyDebug.LOG ) + Log.d(TAG, "default_optical_stabilization: " + default_optical_stabilization); + } + builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_OFF); + } + else if( default_optical_stabilization != null ) { + if( builder.get(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE) != null && !builder.get(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE).equals(default_optical_stabilization) ) { + if( MyDebug.LOG ) + Log.d(TAG, "set optical stabilization back to: " + default_optical_stabilization); + builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, default_optical_stabilization); + } + } + } } private float getLogProfile(float in) { //final float black_level = 4.0f/255.0f; - final float power = 1.0f/2.2f; + //final float power = 1.0f/2.2f; final float log_A = log_profile_strength; /*float out; if( in <= black_level ) { @@ -751,81 +851,268 @@ public class CameraController2 extends CameraController { float out = (float) (Math.log1p(log_A * in) / Math.log1p(log_A)); // apply gamma - out = (float)Math.pow(out, power); + // update: no longer need to do this with improvements made in 1.48 onwards + //out = (float)Math.pow(out, power); //out = Math.max(out, 0.5f); return out; } - private void setLogProfile(CaptureRequest.Builder builder) { + private float getGammaProfile(float in) { + return (float)Math.pow(in, 1.0f/gamma_profile); + } + + private void setTonemapProfile(CaptureRequest.Builder builder) { if( MyDebug.LOG ) { - Log.d(TAG, "setLogProfile"); - Log.d(TAG, "use_log_profile: " + use_log_profile); + Log.d(TAG, "setTonemapProfile"); + Log.d(TAG, "tonemap_profile: " + tonemap_profile); Log.d(TAG, "log_profile_strength: " + log_profile_strength); + Log.d(TAG, "gamma_profile: " + gamma_profile); Log.d(TAG, "default_tonemap_mode: " + default_tonemap_mode); } - if( use_log_profile && log_profile_strength > 0.0f ) { + boolean have_tonemap_profile = tonemap_profile != TonemapProfile.TONEMAPPROFILE_OFF; + if( tonemap_profile == TonemapProfile.TONEMAPPROFILE_LOG && log_profile_strength == 0.0f ) + have_tonemap_profile = false; + else if( tonemap_profile == TonemapProfile.TONEMAPPROFILE_GAMMA && gamma_profile == 0.0f ) + have_tonemap_profile = false; + + // to use test_new, also need to uncomment the test code in setFocusValue() to call setTonemapProfile() + //boolean test_new = this.af_mode == CaptureRequest.CONTROL_AF_MODE_AUTO; // testing + + //if( test_new ) + // have_tonemap_profile = false; + + if( have_tonemap_profile ) { if( default_tonemap_mode == null ) { // save the default tonemap_mode default_tonemap_mode = builder.get(CaptureRequest.TONEMAP_MODE); if( MyDebug.LOG ) Log.d(TAG, "default_tonemap_mode: " + default_tonemap_mode); } - // if changing this, make sure we don't exceed tonemap_max_curve_points_c - // we want: - // 0-15: step 1 (16 values) - // 16-47: step 2 (16 values) - // 48-111: step 4 (16 values) - // 112-231 : step 8 (15 values) - // 232-255: step 24 (1 value) - int step = 1, c = 0; - float [] values = new float[2*tonemap_max_curve_points_c]; - for(int i=0;i<232;i+=step) { - float in = ((float)i) / 255.0f; - float out = getLogProfile(in); - values[c++] = in; - values[c++] = out; - if( (c/2) % 16 == 0 ) { - step *= 2; - } + + final boolean use_preset_curve = true; + //final boolean use_preset_curve = false; // test + //final boolean use_preset_curve = test_new; // test + if( use_preset_curve && tonemap_profile == TonemapProfile.TONEMAPPROFILE_REC709 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.d(TAG, "set TONEMAP_PRESET_CURVE_REC709"); + builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_PRESET_CURVE); + builder.set(CaptureRequest.TONEMAP_PRESET_CURVE, CaptureRequest.TONEMAP_PRESET_CURVE_REC709); } - values[c++] = 1.0f; - values[c++] = getLogProfile(1.0f); - /*{ - int n_values = 257; - float [] values = new float [2*n_values]; - for(int i=0;i= Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.d(TAG, "set TONEMAP_PRESET_CURVE_SRGB"); + builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_PRESET_CURVE); + builder.set(CaptureRequest.TONEMAP_PRESET_CURVE, CaptureRequest.TONEMAP_PRESET_CURVE_SRGB); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "handle via TONEMAP_MODE_CONTRAST_CURVE / TONEMAP_CURVE"); + float [] values = null; + switch( tonemap_profile ) { + case TONEMAPPROFILE_REC709: + // y = 4.5x if x < 0.018, else y = 1.099*x^0.45 - 0.099 + float [] x_values = new float[] { + 0.0000f, 0.0667f, 0.1333f, 0.2000f, + 0.2667f, 0.3333f, 0.4000f, 0.4667f, + 0.5333f, 0.6000f, 0.6667f, 0.7333f, + 0.8000f, 0.8667f, 0.9333f, 1.0000f + }; + values = new float[2*x_values.length]; + int c = 0; + for(float x_value : x_values) { + float out; + if( x_value < 0.018f ) { + out = 4.5f * x_value; + } + else { + out = (float)(1.099*Math.pow(x_value, 0.45) - 0.099); + } + values[c++] = x_value; + values[c++] = out; + } + break; + case TONEMAPPROFILE_SRGB: + values = new float [] { + 0.0000f, 0.0000f, 0.0667f, 0.2864f, 0.1333f, 0.4007f, 0.2000f, 0.4845f, + 0.2667f, 0.5532f, 0.3333f, 0.6125f, 0.4000f, 0.6652f, 0.4667f, 0.7130f, + 0.5333f, 0.7569f, 0.6000f, 0.7977f, 0.6667f, 0.8360f, 0.7333f, 0.8721f, + 0.8000f, 0.9063f, 0.8667f, 0.9389f, 0.9333f, 0.9701f, 1.0000f, 1.0000f + }; + break; + case TONEMAPPROFILE_LOG: + case TONEMAPPROFILE_GAMMA: + { + // better to use uniformly spaced values, otherwise we get a weird looking effect - this can be + // seen most prominently when using gamma 1.0f, which should look linear (and hence be independent + // of the x values we use) + // can be reproduced on at least OnePlus 3T and Galaxy S10e (although the exact behaviour of the + // poor results is different on those devices) + int n_values = tonemap_log_max_curve_points_c; + if( is_samsung ) { + // unfortunately odd bug on Samsung devices (at least S7 and S10e) where if more than 32 control points, + // the maximum brightness value is reduced (can best be seen with 64 points, and using gamma==1.0) + // note that Samsung devices also need at least 16 control points - or in some cases 32, see comments for + // enforceMinTonemapCurvePoints(). + // 32 is better than 16 anyway, as better to have more points for finer curve where possible. + n_values = 32; + } + //int n_values = test_new ? 32 : 128; + //int n_values = 32; + if( MyDebug.LOG ) + Log.d(TAG, "n_values: " + n_values); + values = new float [2*n_values]; + for(int i=0;i 255 ) @@ -865,7 +1152,7 @@ public class CameraController2 extends CameraController { } else { green = temperature - 60; - green = (float)(288.1221695283 * (Math.pow((double) green, -0.0755148492))); + green = (float)(288.1221695283 * (Math.pow(green, -0.0755148492))); if (green < 0) green = 0; if (green > 255) @@ -1456,9 +1743,12 @@ public class CameraController2 extends CameraController { this.preview_error_cb = preview_error_cb; this.camera_error_cb = camera_error_cb; + this.is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung"); this.is_samsung_s7 = Build.MODEL.toLowerCase(Locale.US).contains("sm-g93"); - if( MyDebug.LOG ) + if( MyDebug.LOG ) { + Log.d(TAG, "is_samsung: " + is_samsung); Log.d(TAG, "is_samsung_s7: " + is_samsung_s7); + } thread = new HandlerThread("CameraBackground"); thread.start(); @@ -1487,10 +1777,26 @@ public class CameraController2 extends CameraController { Log.d(TAG, "successfully obtained camera characteristics"); // now read cached values characteristics_sensor_orientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - characteristics_is_front_facing = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT; + + switch( characteristics.get(CameraCharacteristics.LENS_FACING) ) { + case CameraMetadata.LENS_FACING_FRONT: + characteristics_facing = Facing.FACING_FRONT; + break; + case CameraMetadata.LENS_FACING_BACK: + characteristics_facing = Facing.FACING_BACK; + break; + case CameraMetadata.LENS_FACING_EXTERNAL: + characteristics_facing = Facing.FACING_EXTERNAL; + break; + default: + Log.e(TAG, "unknown camera_facing: " + characteristics.get(CameraCharacteristics.LENS_FACING)); + characteristics_facing = Facing.FACING_UNKNOWN; + break; + } + if( MyDebug.LOG ) { Log.d(TAG, "characteristics_sensor_orientation: " + characteristics_sensor_orientation); - Log.d(TAG, "characteristics_is_front_facing: " + characteristics_is_front_facing); + Log.d(TAG, "characteristics_facing: " + characteristics_facing); } CameraController2.this.camera = cam; @@ -1714,6 +2020,11 @@ public class CameraController2 extends CameraController { media_action_sound.load(MediaActionSound.START_VIDEO_RECORDING); media_action_sound.load(MediaActionSound.STOP_VIDEO_RECORDING); media_action_sound.load(MediaActionSound.SHUTTER_CLICK); + + // expand tonemap curves + jtvideo_values = enforceMinTonemapCurvePoints(jtvideo_values_base); + jtlog_values = enforceMinTonemapCurvePoints(jtlog_values_base); + jtlog2_values = enforceMinTonemapCurvePoints(jtlog2_values_base); } @Override @@ -1752,7 +2063,76 @@ public class CameraController2 extends CameraController { } } } - + + /** Enforce a minimum number of points in tonemap curves - needed due to Galaxy S10e having wrong behaviour if fewer + * than 16 or in some cases 32 points?! OnePlus 3T meanwhile has more gradual behaviour where it gets better at 64 points. + */ + private float [] enforceMinTonemapCurvePoints(float[] in_values) { + if( MyDebug.LOG ) { + Log.d(TAG, "enforceMinTonemapCurvePoints: " + Arrays.toString(in_values)); + Log.d(TAG, "length: " + in_values.length/2); + } + int min_points_c = 64; + if( is_samsung ) { + // Unfortunately odd bug on Samsung devices (at least S7 and S10e) where if more than 32 control points, + // the maximum brightness value is reduced (can best be seen with 64 points, and using gamma==1.0). + // Also note that Samsung devices also need at least 16 control points, or in some cases 32, due to problem + // where things come out almost all black with some white. So choose 32! + //min_points_c = 16; + min_points_c = 32; + } + if( MyDebug.LOG ) + Log.d(TAG, "min_points_c: " + min_points_c); + if( in_values.length >= 2*min_points_c ) { + if( MyDebug.LOG ) + Log.d(TAG, "already enough points"); + return in_values; // fine + } + List> points = new ArrayList<>(); + for(int i=0;i point = new Pair<>(in_values[2*i], in_values[2*i+1]); + points.add(point); + } + if( points.size() < 2 ) { + Log.e(TAG, "less than 2 points?!"); + return in_values; + } + + while( points.size() < min_points_c ) { + // find largest interval, and subdivide + int largest_indx = 0; + float largest_dist = 0.0f; + for(int i=0;i p0 = points.get(i); + Pair p1 = points.get(i+1); + float dist = p1.first - p0.first; + if( dist > largest_dist ) { + largest_indx = i; + largest_dist = dist; + } + } + /*if( MyDebug.LOG ) + Log.d(TAG, "largest indx " + largest_indx + " dist: " + largest_dist);*/ + Pair p0 = points.get(largest_indx); + Pair p1 = points.get(largest_indx+1); + float mid_x = 0.5f*(p0.first + p1.first); + float mid_y = 0.5f*(p0.second + p1.second); + /*if( MyDebug.LOG ) + Log.d(TAG, " insert: " + mid_x + " , " + mid_y);*/ + points.add(largest_indx+1, new Pair<>(mid_x, mid_y)); + } + + float [] out_values = new float[2*points.size()]; + for(int i=0;i point = points.get(i); + out_values[2*i] = point.first; + out_values[2*i+1] = point.second; + /*if( MyDebug.LOG ) + Log.d(TAG, "out point[" + i + "]: " + point.first + " , " + point.second);*/ + } + return out_values; + } + private void closePictureImageReader() { if( MyDebug.LOG ) Log.d(TAG, "closePictureImageReader()"); @@ -1948,20 +2328,6 @@ public class CameraController2 extends CameraController { } } - if( MyDebug.LOG ) { - int[] ois_modes = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION); // may be null on some devices - if (ois_modes != null) { - for (int ois_mode : ois_modes) { - if (MyDebug.LOG) - Log.d(TAG, "ois mode: " + ois_mode); - if (ois_mode == CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON) { - if (MyDebug.LOG) - Log.d(TAG, "supports ois"); - } - } - } - } - int [] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); //boolean capabilities_manual_sensor = false; boolean capabilities_manual_post_processing = false; @@ -2039,10 +2405,14 @@ public class CameraController2 extends CameraController { // (and we don't want to set supports_burst to false for such a resolution). boolean found = false; for(android.util.Size sz : camera_picture_sizes) { - if( sz.equals(camera_size) ) + if( sz.equals(camera_size) ) { found = true; + break; + } } if( !found ) { + if( MyDebug.LOG ) + Log.d(TAG, "high resolution [non-burst] picture size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); CameraController.Size size = new CameraController.Size(camera_size.getWidth(), camera_size.getHeight()); size.supports_burst = false; camera_features.picture_sizes.add(size); @@ -2050,10 +2420,17 @@ public class CameraController2 extends CameraController { } } } - for(android.util.Size camera_size : camera_picture_sizes) { - if( MyDebug.LOG ) - Log.d(TAG, "picture size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); - camera_features.picture_sizes.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); + if( camera_picture_sizes == null ) { + // camera_picture_sizes is null on Samsung Galaxy Note 10+ and S20 for camera ID 4! + Log.e(TAG, "no picture sizes returned by getOutputSizes"); + throw new CameraControllerException(); + } + else { + for(android.util.Size camera_size : camera_picture_sizes) { + if( MyDebug.LOG ) + Log.d(TAG, "picture size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); + camera_features.picture_sizes.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); + } } // sizes are usually already sorted from high to low, but sort just in case // note some devices do have sizes in a not fully sorted order (e.g., Nokia 8) @@ -2111,17 +2488,24 @@ public class CameraController2 extends CameraController { for(int[] r : this.ae_fps_ranges) { min_fps = Math.min(min_fps, r[0]); } - for(android.util.Size camera_size : camera_video_sizes) { - if( camera_size.getWidth() > 4096 || camera_size.getHeight() > 2160 ) - continue; // Nexus 6 returns these, even though not supported?! - long mfd = configs.getOutputMinFrameDuration(MediaRecorder.class, camera_size); - int max_fps = (int)((1.0 / mfd) * 1000000000L); - ArrayList fr = new ArrayList<>(); - fr.add(new int[] {min_fps, max_fps}); - CameraController.Size normal_video_size = new CameraController.Size(camera_size.getWidth(), camera_size.getHeight(), fr, false); - camera_features.video_sizes.add(normal_video_size); - if( MyDebug.LOG ) { - Log.d(TAG, "normal video size: " + normal_video_size); + if( camera_video_sizes == null ) { + // camera_video_sizes is null on Samsung Galaxy Note 10+ and S20 for camera ID 4! + Log.e(TAG, "no video sizes returned by getOutputSizes"); + throw new CameraControllerException(); + } + else { + for(android.util.Size camera_size : camera_video_sizes) { + if( camera_size.getWidth() > 4096 || camera_size.getHeight() > 2160 ) + continue; // Nexus 6 returns these, even though not supported?! + long mfd = configs.getOutputMinFrameDuration(MediaRecorder.class, camera_size); + int max_fps = (int)((1.0 / mfd) * 1000000000L); + ArrayList fr = new ArrayList<>(); + fr.add(new int[] {min_fps, max_fps}); + CameraController.Size normal_video_size = new CameraController.Size(camera_size.getWidth(), camera_size.getHeight(), fr, false); + camera_features.video_sizes.add(normal_video_size); + if( MyDebug.LOG ) { + Log.d(TAG, "normal video size: " + normal_video_size); + } } } Collections.sort(camera_features.video_sizes, new CameraController.SizeSorter()); @@ -2170,22 +2554,30 @@ public class CameraController2 extends CameraController { // would be good to not assume Open Camera runs in landscape mode (if we ever ran in portrait mode, // we'd still want display_size.x > display_size.y as preview resolutions also have width > height) if( display_size.x < display_size.y ) { + //noinspection SuspiciousNameCombination display_size.set(display_size.y, display_size.x); } if( MyDebug.LOG ) Log.d(TAG, "display_size: " + display_size.x + " x " + display_size.y); } - for(android.util.Size camera_size : camera_preview_sizes) { - if( MyDebug.LOG ) - Log.d(TAG, "preview size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); - if( camera_size.getWidth() > display_size.x || camera_size.getHeight() > display_size.y ) { - // Nexus 6 returns these, even though not supported?! (get green corruption lines if we allow these) - // Google Camera filters anything larger than height 1080, with a todo saying to use device's measurements - continue; + if( camera_preview_sizes == null ) { + // camera_preview_sizes is null on Samsung Galaxy Note 10+ and S20 for camera ID 4! + Log.e(TAG, "no preview sizes returned by getOutputSizes"); + throw new CameraControllerException(); + } + else { + for(android.util.Size camera_size : camera_preview_sizes) { + if( MyDebug.LOG ) + Log.d(TAG, "preview size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); + if( camera_size.getWidth() > display_size.x || camera_size.getHeight() > display_size.y ) { + // Nexus 6 returns these, even though not supported?! (get green corruption lines if we allow these) + // Google Camera filters anything larger than height 1080, with a todo saying to use device's measurements + continue; + } + camera_features.preview_sizes.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); } - camera_features.preview_sizes.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); } - + if( characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ) { camera_features.supported_flash_values = new ArrayList<>(); camera_features.supported_flash_values.add("flash_off"); @@ -2196,7 +2588,7 @@ public class CameraController2 extends CameraController { camera_features.supported_flash_values.add("flash_red_eye"); } } - else if( isFrontFacing() ) { + else if( (getFacing() == Facing.FACING_FRONT) ) { camera_features.supported_flash_values = new ArrayList<>(); camera_features.supported_flash_values.add("flash_off"); camera_features.supported_flash_values.add("flash_frontscreen_auto"); @@ -2225,12 +2617,27 @@ public class CameraController2 extends CameraController { camera_features.is_white_balance_lock_supported = true; + camera_features.is_optical_stabilization_supported = false; + int [] supported_optical_stabilization_modes = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION); + if( supported_optical_stabilization_modes != null ) { + for(int supported_optical_stabilization_mode : supported_optical_stabilization_modes) { + if( supported_optical_stabilization_mode == CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON ) { + camera_features.is_optical_stabilization_supported = true; + break; + } + } + } + if( MyDebug.LOG ) + Log.d(TAG, "is_optical_stabilization_supported: " + camera_features.is_optical_stabilization_supported); + supports_optical_stabilization = camera_features.is_optical_stabilization_supported; + camera_features.is_video_stabilization_supported = false; int [] supported_video_stabilization_modes = characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES); if( supported_video_stabilization_modes != null ) { for(int supported_video_stabilization_mode : supported_video_stabilization_modes) { if( supported_video_stabilization_mode == CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON ) { camera_features.is_video_stabilization_supported = true; + break; } } } @@ -2243,6 +2650,10 @@ public class CameraController2 extends CameraController { int [] white_balance_modes = characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES); if( white_balance_modes != null ) { for(int value : white_balance_modes) { + // n.b., Galaxy S10e for front and ultra-wide cameras offers CONTROL_AWB_MODE_OFF despite + // capabilities_manual_post_processing==false; if we don't check for capabilities_manual_post_processing, + // adjusting white balance temperature seems to work, but seems safest to require + // capabilities_manual_post_processing anyway if( value == CameraMetadata.CONTROL_AWB_MODE_OFF && capabilities_manual_post_processing && allowManualWB() ) { camera_features.supports_white_balance_temperature = true; camera_features.min_temperature = min_white_balance_temperature_c; @@ -2250,6 +2661,7 @@ public class CameraController2 extends CameraController { } } } + supports_white_balance_temperature = camera_features.supports_white_balance_temperature; // see note above //if( capabilities_manual_sensor ) @@ -2285,7 +2697,14 @@ public class CameraController2 extends CameraController { if( MyDebug.LOG ) Log.d(TAG, "tonemap_max_curve_points: " + tonemap_max_curve_points); camera_features.tonemap_max_curve_points = tonemap_max_curve_points; - camera_features.supports_tonemap_curve = tonemap_max_curve_points >= tonemap_max_curve_points_c; + // for now we only expose supporting of custom tonemap curves if there are enough curve points for all the + // profiles we support + // remember to divide by 2 if we're comparing against the raw array length! + camera_features.supports_tonemap_curve = + tonemap_max_curve_points >= tonemap_log_max_curve_points_c && + tonemap_max_curve_points >= jtvideo_values.length/2 && + tonemap_max_curve_points >= jtlog_values.length/2 && + tonemap_max_curve_points >= jtlog2_values.length/2; } else { if( MyDebug.LOG ) @@ -2295,28 +2714,19 @@ public class CameraController2 extends CameraController { if( MyDebug.LOG ) Log.d(TAG, "supports_tonemap_curve?: " + camera_features.supports_tonemap_curve); - { - // Calculate view angles - // Note this is an approximation (see http://stackoverflow.com/questions/39965408/what-is-the-android-camera2-api-equivalent-of-camera-parameters-gethorizontalvie ). - // This does not take into account the aspect ratio of the preview or camera, it's up to the caller to do this (e.g., see Preview.getViewAngleX(), getViewAngleY()). - Rect active_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - SizeF physical_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE); - android.util.Size pixel_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); - float [] focal_lengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS); - //camera_features.view_angle_x = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getWidth(), (2.0 * focal_lengths[0]))); - //camera_features.view_angle_y = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getHeight(), (2.0 * focal_lengths[0]))); - float frac_x = ((float)active_size.width())/(float)pixel_size.getWidth(); - float frac_y = ((float)active_size.height())/(float)pixel_size.getHeight(); - camera_features.view_angle_x = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getWidth() * frac_x, (2.0 * focal_lengths[0]))); - camera_features.view_angle_y = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getHeight() * frac_y, (2.0 * focal_lengths[0]))); - if( MyDebug.LOG ) { - Log.d(TAG, "frac_x: " + frac_x); - Log.d(TAG, "frac_y: " + frac_y); - Log.d(TAG, "view_angle_x: " + camera_features.view_angle_x); - Log.d(TAG, "view_angle_y: " + camera_features.view_angle_y); - } + float [] apertures = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_APERTURES); + //float [] apertures = new float[]{1.5f, 1.9f, 2.0f, 2.2f, 2.4f, 4.0f, 8.0f, 16.0f}; // test + if( MyDebug.LOG ) + Log.d(TAG, "apertures: " + Arrays.toString(apertures)); + // no point supporting if only a single aperture + if( apertures != null && apertures.length > 1 ) { + camera_features.apertures = apertures; } + SizeF view_angle = CameraControllerManager2.computeViewAngles(characteristics); + camera_features.view_angle_x = view_angle.getWidth(); + camera_features.view_angle_y = view_angle.getHeight(); + return camera_features; } @@ -2680,7 +3090,7 @@ public class CameraController2 extends CameraController { for(int value2 : values2) { String this_value = convertWhiteBalance(value2); if( this_value != null ) { - if( value2 == CameraMetadata.CONTROL_AWB_MODE_OFF && !allowManualWB() ) { + if( value2 == CameraMetadata.CONTROL_AWB_MODE_OFF && !supports_white_balance_temperature ) { // filter } else { @@ -3140,6 +3550,7 @@ public class CameraController2 extends CameraController { camera_settings.has_iso = false; camera_settings.iso = 0; } + updateUseFakePrecaptureMode(camera_settings.flash_value); if( camera_settings.setAEMode(previewBuilder, false) ) { setRepeatingRequest(); @@ -3228,6 +3639,33 @@ public class CameraController2 extends CameraController { return true; } + @Override + public void setAperture(float aperture) { + if( MyDebug.LOG ) { + Log.d(TAG, "setAperture: " + aperture); + Log.d(TAG, "current aperture: " + camera_settings.aperture); + } + if( camera_settings.has_aperture && camera_settings.aperture == aperture ) { + if( MyDebug.LOG ) + Log.d(TAG, "already set"); + } + try { + camera_settings.has_aperture = true; + camera_settings.aperture = aperture; + if( camera_settings.setAperture(previewBuilder) ) { + setRepeatingRequest(); + } + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set aperture"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + @Override public Size getPictureSize() { return new Size(picture_width, picture_height); @@ -3607,11 +4045,6 @@ public class CameraController2 extends CameraController { public void setPreviewSize(int width, int height) { if( MyDebug.LOG ) Log.d(TAG, "setPreviewSize: " + width + " , " + height); - /*if( texture != null ) { - if( MyDebug.LOG ) - Log.d(TAG, "set size of preview texture"); - texture.setDefaultBufferSize(width, height); - }*/ preview_width = width; preview_height = height; /*if( previewImageReader != null ) { @@ -3623,8 +4056,10 @@ public class CameraController2 extends CameraController { @Override public void setVideoStabilization(boolean enabled) { + if( MyDebug.LOG ) + Log.d(TAG, "setVideoStabilization: " + enabled); camera_settings.video_stabilization = enabled; - camera_settings.setVideoStabilization(previewBuilder); + camera_settings.setStabilization(previewBuilder); try { setRepeatingRequest(); } @@ -3638,25 +4073,44 @@ public class CameraController2 extends CameraController { } } + @Override + public boolean getOpticalStabilization() { + Integer ois_mode = previewBuilder.get(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE); + if( ois_mode == null ) + return false; + return( ois_mode == CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON ); + } + @Override public boolean getVideoStabilization() { return camera_settings.video_stabilization; } @Override - public void setLogProfile(boolean use_log_profile, float log_profile_strength) { + public void setTonemapProfile(TonemapProfile tonemap_profile, float log_profile_strength, float gamma) { if( MyDebug.LOG ) { - Log.d(TAG, "setLogProfile: " + use_log_profile); + Log.d(TAG, "setTonemapProfile: " + tonemap_profile); Log.d(TAG, "log_profile_strength: " + log_profile_strength); + Log.d(TAG, "gamma: " + gamma); } - if( camera_settings.use_log_profile == use_log_profile && camera_settings.log_profile_strength == log_profile_strength ) + if( camera_settings.tonemap_profile == tonemap_profile && + camera_settings.log_profile_strength == log_profile_strength && + camera_settings.gamma_profile == gamma ) return; // no change - camera_settings.use_log_profile = use_log_profile; - if( use_log_profile ) + + camera_settings.tonemap_profile = tonemap_profile; + + if( tonemap_profile == TonemapProfile.TONEMAPPROFILE_LOG ) camera_settings.log_profile_strength = log_profile_strength; else camera_settings.log_profile_strength = 0.0f; - camera_settings.setLogProfile(previewBuilder); + + if( tonemap_profile == TonemapProfile.TONEMAPPROFILE_GAMMA ) + camera_settings.gamma_profile = gamma; + else + camera_settings.gamma_profile = 0.0f; + + camera_settings.setTonemapProfile(previewBuilder); try { setRepeatingRequest(); } @@ -3671,8 +4125,8 @@ public class CameraController2 extends CameraController { } @Override - public boolean isLogProfile() { - return camera_settings.use_log_profile; + public TonemapProfile getTonemapProfile() { + return camera_settings.tonemap_profile; } /** For testing. @@ -3900,6 +4354,7 @@ public class CameraController2 extends CameraController { camera_settings.af_mode = focus_mode; camera_settings.setFocusMode(previewBuilder); camera_settings.setFocusDistance(previewBuilder); // also need to set distance, in case changed between infinity, manual or other modes + //camera_settings.setTonemapProfile(previewBuilder); // testing - if using focus mode to test video profiles, see test_new flag try { setRepeatingRequest(); } @@ -4028,6 +4483,8 @@ public class CameraController2 extends CameraController { } else if( burst_type != BurstType.BURSTTYPE_NONE ) use_fake_precapture_mode = true; + else if( camera_settings.has_iso ) + use_fake_precapture_mode = true; else { use_fake_precapture_mode = use_fake_precapture; } @@ -4148,8 +4605,9 @@ public class CameraController2 extends CameraController { @Override public void setLocationInfo(Location location) { + // don't log location, in case of privacy! if( MyDebug.LOG ) - Log.d(TAG, "setLocationInfo: " + location.getLongitude() + " , " + location.getLatitude()); + Log.d(TAG, "setLocationInfo"); this.camera_settings.location = location; } @@ -4420,7 +4878,7 @@ public class CameraController2 extends CameraController { } @Override - public void setPreviewDisplay(SurfaceHolder holder) throws CameraControllerException { + public void setPreviewDisplay(SurfaceHolder holder) { if( MyDebug.LOG ) { Log.d(TAG, "setPreviewDisplay"); Log.e(TAG, "SurfaceHolder not supported for CameraController2!"); @@ -4430,9 +4888,11 @@ public class CameraController2 extends CameraController { } @Override - public void setPreviewTexture(TextureView texture) throws CameraControllerException { - if( MyDebug.LOG ) - Log.d(TAG, "setPreviewTexture"); + public void setPreviewTexture(TextureView texture) { + if( MyDebug.LOG ) { + Log.d(TAG, "setPreviewTexture: " + texture); + Log.d(TAG, "surface: " + texture.getSurfaceTexture()); + } if( this.texture != null ) { if( MyDebug.LOG ) Log.d(TAG, "preview texture already set"); @@ -4526,6 +4986,25 @@ public class CameraController2 extends CameraController { return surface_texture; } + @Override + public void updatePreviewTexture() { + if( MyDebug.LOG ) + Log.d(TAG, "updatePreviewTexture"); + if( texture != null ) { + if( preview_width == 0 || preview_height == 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "preview size not yet set"); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "preview size: " + preview_width + " x " + preview_height); + this.test_texture_view_buffer_w = preview_width; + this.test_texture_view_buffer_h = preview_height; + texture.setDefaultBufferSize(preview_width, preview_height); + } + } + } + private void createCaptureSession(final MediaRecorder video_recorder, boolean want_photo_video_recording) throws CameraControllerException { if( MyDebug.LOG ) Log.d(TAG, "create capture session"); @@ -4567,13 +5046,13 @@ public class CameraController2 extends CameraController { if( texture != null ) { // need to set the texture size if( MyDebug.LOG ) - Log.d(TAG, "set size of preview texture"); + Log.d(TAG, "set size of preview texture: " + preview_width + " x " + preview_height); if( preview_width == 0 || preview_height == 0 ) { if( MyDebug.LOG ) Log.e(TAG, "application needs to call setPreviewSize()"); throw new RuntimeException(); // throw as RuntimeException, as this is a programming error } - texture.setDefaultBufferSize(preview_width, preview_height); + updatePreviewTexture(); // also need to create a new surface for the texture, in case the size has changed - but make sure we remove the old one first! synchronized( background_camera_lock ) { if( surface_texture != null ) { @@ -4597,7 +5076,7 @@ public class CameraController2 extends CameraController { /*if( MyDebug.LOG ) Log.d(TAG, "preview size: " + previewImageReader.getWidth() + " x " + previewImageReader.getHeight());*/ if( MyDebug.LOG ) - Log.d(TAG, "preview size: " + this.preview_width + " x " + this.preview_height); + Log.d(TAG, "set preview size: " + this.preview_width + " x " + this.preview_height); synchronized( background_camera_lock ) { if( video_recorder != null ) @@ -4629,9 +5108,16 @@ public class CameraController2 extends CameraController { synchronized( background_camera_lock ) { captureSession = session; Surface surface = getPreviewSurface(); + if( MyDebug.LOG ) { + Log.d(TAG, "add surface to previewBuilder: " + surface); + } previewBuilder.addTarget(surface); - if( video_recorder != null ) + if( video_recorder != null ) { + if( MyDebug.LOG ) { + Log.d(TAG, "add video recorder surface to previewBuilder: " + video_recorder_surface); + } previewBuilder.addTarget(video_recorder_surface); + } try { setRepeatingRequest(); } @@ -4710,6 +5196,11 @@ public class CameraController2 extends CameraController { } // n.b., raw not supported for photo snapshots while video recording } + else if( want_video_high_speed ) { + // future proofing - at the time of writing want_video_high_speed is only set when recording video, + // but if ever this is changed, can only support the preview_surface as a target + surfaces = Collections.singletonList(preview_surface); + } else if( imageReaderRaw != null ) { surfaces = Arrays.asList(preview_surface, imageReader.getSurface(), imageReaderRaw.getSurface()); } @@ -4738,6 +5229,7 @@ public class CameraController2 extends CameraController { } } if( video_recorder != null && want_video_high_speed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { + //if( want_video_high_speed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { camera.createConstrainedHighSpeedCaptureSession(surfaces, myStateCallback, handler); @@ -5026,7 +5518,7 @@ public class CameraController2 extends CameraController { this.capture_follows_autofocus_hint = capture_follows_autofocus_hint; this.autofocus_cb = cb; try { - if( use_fake_precapture_mode && !camera_settings.has_iso ) { + if( use_fake_precapture_mode ) { boolean want_flash = false; if( camera_settings.flash_value.equals("flash_auto") || camera_settings.flash_value.equals("flash_frontscreen_auto") ) { // calling fireAutoFlash() also caches the decision on whether to flash - otherwise if the flash fires now, we'll then think the scene is bright enough to not need the flash! @@ -5039,7 +5531,11 @@ public class CameraController2 extends CameraController { if( want_flash ) { if( MyDebug.LOG ) Log.d(TAG, "turn on torch for fake flash"); - afBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + if( !camera_settings.has_iso ) { + // in auto-mode, need to ensure CONTROL_AE_MODE isn't est to flash auto/on for torch to work + // in manual-mode, fine as CONTROL_AE_MODE will be off + afBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + } afBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); test_fake_flash_focus++; fake_precapture_torch_focus_performed = true; @@ -5389,6 +5885,7 @@ public class CameraController2 extends CameraController { Log.e(TAG, "message: " + e.getMessage()); } e.printStackTrace(); + //noinspection UnusedAssignment ok = false; jpeg_todo = false; raw_todo = false; @@ -5399,6 +5896,7 @@ public class CameraController2 extends CameraController { if( MyDebug.LOG ) Log.d(TAG, "captureSession already closed!"); e.printStackTrace(); + //noinspection UnusedAssignment ok = false; jpeg_todo = false; raw_todo = false; @@ -5793,6 +6291,7 @@ public class CameraController2 extends CameraController { Log.e(TAG, "message: " + e.getMessage()); } e.printStackTrace(); + //noinspection UnusedAssignment ok = false; jpeg_todo = false; raw_todo = false; @@ -5803,6 +6302,7 @@ public class CameraController2 extends CameraController { if( MyDebug.LOG ) Log.d(TAG, "captureSession already closed!"); e.printStackTrace(); + //noinspection UnusedAssignment ok = false; jpeg_todo = false; raw_todo = false; @@ -5910,7 +6410,7 @@ public class CameraController2 extends CameraController { // For Nexus 6, max reported ISO is 1196, so the limit for dark scenes shouldn't be more than this // Nokia 8's max reported ISO is 1551 // Note that OnePlus 3T has max reported ISO of 800, but this is a device bug - if( capture_result_iso >= 1100 ) { + if( capture_result_iso >= ISO_FOR_DARK ) { if( MyDebug.LOG ) Log.d(TAG, "optimise for dark scene"); n_burst = noise_reduction_low_light ? N_IMAGES_NR_DARK_LOW_LIGHT : N_IMAGES_NR_DARK; @@ -6134,6 +6634,7 @@ public class CameraController2 extends CameraController { Log.e(TAG, "message: " + e.getMessage()); } e.printStackTrace(); + //noinspection UnusedAssignment ok = false; jpeg_todo = false; raw_todo = false; @@ -6231,7 +6732,11 @@ public class CameraController2 extends CameraController { case "flash_on": if(MyDebug.LOG) Log.d(TAG, "turn on torch"); - previewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + if( !camera_settings.has_iso ) { + // in auto-mode, need to ensure CONTROL_AE_MODE isn't est to flash auto/on for torch to work + // in manual-mode, fine as CONTROL_AE_MODE will be off + previewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + } previewBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); test_fake_flash_precapture++; fake_precapture_torch_performed = true; @@ -6390,8 +6895,7 @@ public class CameraController2 extends CameraController { Log.d(TAG, "use_fake_precapture_mode: " + use_fake_precapture_mode); } // Don't need precapture if flash off or torch - // And currently has_iso manual mode doesn't support flash - but just in case that's changed later, we still probably don't want to be doing a precapture... - if( camera_settings.has_iso || camera_settings.flash_value.equals("flash_off") || camera_settings.flash_value.equals("flash_torch") ) { + if( camera_settings.flash_value.equals("flash_off") || camera_settings.flash_value.equals("flash_torch") || camera_settings.flash_value.equals("flash_frontscreen_torch") ) { call_takePictureAfterPrecapture = true; } else if( use_fake_precapture_mode ) { @@ -6493,9 +6997,9 @@ public class CameraController2 extends CameraController { } @Override - public boolean isFrontFacing() { + public Facing getFacing() { // cached for performance, as this method is frequently called from Preview.onOrientationChanged - return characteristics_is_front_facing; + return characteristics_facing; } @Override @@ -6617,7 +7121,17 @@ public class CameraController2 extends CameraController { public long captureResultFrameDuration() { return capture_result_frame_duration; } - + + @Override + public boolean captureResultHasAperture() { + return capture_result_has_aperture; + } + + @Override + public float captureResultAperture() { + return capture_result_aperture; + } + /* @Override public boolean captureResultHasFocusDistance() { @@ -7056,6 +7570,18 @@ public class CameraController2 extends CameraController { state = STATE_WAITING_FAKE_PRECAPTURE_DONE; precapture_state_change_time_ms = System.currentTimeMillis(); } + else if( fake_precapture_turn_on_torch_id == null && camera_settings.has_iso && precapture_state_change_time_ms != -1 && System.currentTimeMillis() - precapture_state_change_time_ms > 100 ) { + // When using manual ISO, we can't make use of changes to the ae_state - but at the same time, we don't + // need ISO/exposure to re-adjust anyway. + // If fake_precapture_turn_on_torch_id != null, we still wait for the physical torch to turn on. + // But if fake_precapture_turn_on_torch_id==null (i.e., for flash_frontscreen_torch), just wait a short + // period to ensure the frontscreen flash has enabled. + if( MyDebug.LOG ) { + Log.d(TAG, "fake precapture started after: " + (System.currentTimeMillis() - precapture_state_change_time_ms)); + } + state = STATE_WAITING_FAKE_PRECAPTURE_DONE; + precapture_state_change_time_ms = System.currentTimeMillis(); + } else if( precapture_state_change_time_ms != -1 && System.currentTimeMillis() - precapture_state_change_time_ms > precapture_start_timeout_c ) { // just in case // always log error, so we can look for it when manually testing with logging disabled @@ -7100,15 +7626,15 @@ public class CameraController2 extends CameraController { private void handleContinuousFocusMove(CaptureResult result) { Integer af_state = result.get(CaptureResult.CONTROL_AF_STATE); if( af_state != null && af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN && af_state != last_af_state ) { - if( MyDebug.LOG ) - Log.d(TAG, "continuous focusing started"); + /*if( MyDebug.LOG ) + Log.d(TAG, "continuous focusing started");*/ if( continuous_focus_move_callback != null ) { continuous_focus_move_callback.onContinuousFocusMove(true); } } else if( af_state != null && last_af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN && af_state != last_af_state ) { - if( MyDebug.LOG ) - Log.d(TAG, "continuous focusing stopped"); + /*if( MyDebug.LOG ) + Log.d(TAG, "continuous focusing stopped");*/ if( continuous_focus_move_callback != null ) { continuous_focus_move_callback.onContinuousFocusMove(false); } @@ -7125,10 +7651,10 @@ public class CameraController2 extends CameraController { Log.d(TAG, "processAF discarded outdated frame " + result.getFrameNumber() + " vs " + last_process_frame_number);*/ return; } - long debug_time = 0; + /*long debug_time = 0; if( MyDebug.LOG ) { debug_time = System.currentTimeMillis(); - } + }*/ last_process_frame_number = result.getFrameNumber(); updateCachedAECaptureStatus(result); @@ -7139,8 +7665,8 @@ public class CameraController2 extends CameraController { Integer af_state = result.get(CaptureResult.CONTROL_AF_STATE); if( af_state != null && af_state != last_af_state ) { - if( MyDebug.LOG ) - Log.d(TAG, "CONTROL_AF_STATE changed from " + last_af_state + " to " + af_state); + /*if( MyDebug.LOG ) + Log.d(TAG, "CONTROL_AF_STATE changed from " + last_af_state + " to " + af_state);*/ last_af_state = af_state; } @@ -7252,6 +7778,19 @@ public class CameraController2 extends CameraController { else { capture_result_has_focus_distance = false; }*/ + if( modified_from_camera_settings ) { + // see note above + } + else if( result.get(CaptureResult.LENS_APERTURE) != null ) { + capture_result_has_aperture = true; + capture_result_aperture = result.get(CaptureResult.LENS_APERTURE); + /*if( MyDebug.LOG ) { + Log.d(TAG, "capture_result_aperture: " + capture_result_aperture); + }*/ + } + else { + capture_result_has_aperture = false; + } { RggbChannelVector vector = result.get(CaptureResult.COLOR_CORRECTION_GAINS); if( modified_from_camera_settings ) { diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager.java index a4b9217e73dffeb49aa6cd617cde0f376b7e8cba..340f6d83de221766e318e872c65e532fffb8c887 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager.java @@ -1,9 +1,18 @@ package net.sourceforge.opencamera.cameracontroller; +import android.content.Context; + /** Provides additional support related to the Android camera APIs. This is to * support functionality that doesn't require a camera to have been opened. */ public abstract class CameraControllerManager { public abstract int getNumberOfCameras(); - public abstract boolean isFrontFacing(int cameraId); + /** Returns whether the supplied cameraId is front, back or external. + */ + public abstract CameraController.Facing getFacing(int cameraId); + + /** Tries to return a textual description for the camera, such as front/back, along with extra + * details if possible such as "ultra-wide". Will be null if no description can be determined. + */ + public abstract String getDescription(Context context, int cameraId); } diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager1.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager1.java index e02691fee8269954979e82280e6ca33158da424e..0a08b6fb48e284ba4e321f807bf22d5be0e48777 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager1.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager1.java @@ -1,7 +1,8 @@ package net.sourceforge.opencamera.cameracontroller; -import net.sourceforge.opencamera.MyDebug; +import foundation.e.camera.R; +import android.content.Context; import android.hardware.Camera; import android.util.Log; @@ -14,19 +15,36 @@ public class CameraControllerManager1 extends CameraControllerManager { return Camera.getNumberOfCameras(); } - public boolean isFrontFacing(int cameraId) { + @Override + public CameraController.Facing getFacing(int cameraId) { try { Camera.CameraInfo camera_info = new Camera.CameraInfo(); Camera.getCameraInfo(cameraId, camera_info); - return (camera_info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT); + switch( camera_info.facing ) { + case Camera.CameraInfo.CAMERA_FACING_FRONT: + return CameraController.Facing.FACING_FRONT; + case Camera.CameraInfo.CAMERA_FACING_BACK: + return CameraController.Facing.FACING_BACK; + } + Log.e(TAG, "unknown camera_facing: " + camera_info.facing); } catch(RuntimeException e) { // Had a report of this crashing on Galaxy Nexus - may be device specific issue, see http://stackoverflow.com/questions/22383708/java-lang-runtimeexception-fail-to-get-camera-info // but good to catch it anyway - if( MyDebug.LOG ) - Log.d(TAG, "failed to set parameters"); + Log.e(TAG, "failed to get facing"); e.printStackTrace(); - return false; } + return CameraController.Facing.FACING_UNKNOWN; + } + + @Override + public String getDescription(Context context, int cameraId) { + switch( getFacing(cameraId) ) { + case FACING_FRONT: + return context.getResources().getString(R.string.front_camera); + case FACING_BACK: + return context.getResources().getString(R.string.back_camera); + } + return null; } } diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java index 27032a2670ad7da1a170c59f95b70a14d6431524..13b4a980970204a8a6cbfce02210d1351c246b42 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java @@ -1,14 +1,17 @@ package net.sourceforge.opencamera.cameracontroller; import net.sourceforge.opencamera.MyDebug; +import foundation.e.camera.R; import android.annotation.TargetApi; import android.content.Context; +import android.graphics.Rect; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; import android.os.Build; import android.util.Log; +import android.util.SizeF; /** Provides support using Android 5's Camera 2 API * android.hardware.camera2.*. @@ -42,12 +45,20 @@ public class CameraControllerManager2 extends CameraControllerManager { } @Override - public boolean isFrontFacing(int cameraId) { + public CameraController.Facing getFacing(int cameraId) { CameraManager manager = (CameraManager)context.getSystemService(Context.CAMERA_SERVICE); try { String cameraIdS = manager.getCameraIdList()[cameraId]; CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraIdS); - return characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + switch( characteristics.get(CameraCharacteristics.LENS_FACING) ) { + case CameraMetadata.LENS_FACING_FRONT: + return CameraController.Facing.FACING_FRONT; + case CameraMetadata.LENS_FACING_BACK: + return CameraController.Facing.FACING_BACK; + case CameraMetadata.LENS_FACING_EXTERNAL: + return CameraController.Facing.FACING_EXTERNAL; + } + Log.e(TAG, "unknown camera_facing: " + characteristics.get(CameraCharacteristics.LENS_FACING)); } catch(Throwable e) { // in theory we should only get CameraAccessException, but Google Play shows we can get a variety of exceptions @@ -58,7 +69,71 @@ public class CameraControllerManager2 extends CameraControllerManager { Log.e(TAG, "exception trying to get camera characteristics"); e.printStackTrace(); } - return false; + return CameraController.Facing.FACING_UNKNOWN; + } + + @Override + public String getDescription(Context context, int cameraId) { + CameraManager manager = (CameraManager)context.getSystemService(Context.CAMERA_SERVICE); + String description = null; + try { + String cameraIdS = manager.getCameraIdList()[cameraId]; + CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraIdS); + + switch( characteristics.get(CameraCharacteristics.LENS_FACING) ) { + case CameraMetadata.LENS_FACING_FRONT: + description = context.getResources().getString(R.string.front_camera); + break; + case CameraMetadata.LENS_FACING_BACK: + description = context.getResources().getString(R.string.back_camera); + break; + case CameraMetadata.LENS_FACING_EXTERNAL: + description = context.getResources().getString(R.string.external_camera); + break; + default: + Log.e(TAG, "unknown camera type"); + return null; + } + + SizeF view_angle = CameraControllerManager2.computeViewAngles(characteristics); + if( view_angle.getWidth() > 90.5f ) { + // count as ultra-wide + description += ", " + context.getResources().getString(R.string.ultrawide); + } + } + catch(Throwable e) { + // see note under isFrontFacing() why we catch anything, not just CameraAccessException + if( MyDebug.LOG ) + Log.e(TAG, "exception trying to get camera characteristics"); + e.printStackTrace(); + } + return description; + } + + /** Helper class to compute view angles from the CameraCharacteristics. + * @return The width and height of the returned size represent the x and y view angles in + * degrees. + */ + static SizeF computeViewAngles(CameraCharacteristics characteristics) { + // Note this is an approximation (see http://stackoverflow.com/questions/39965408/what-is-the-android-camera2-api-equivalent-of-camera-parameters-gethorizontalvie ). + // This does not take into account the aspect ratio of the preview or camera, it's up to the caller to do this (e.g., see Preview.getViewAngleX(), getViewAngleY()). + Rect active_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + SizeF physical_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE); + android.util.Size pixel_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + float [] focal_lengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS); + //camera_features.view_angle_x = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getWidth(), (2.0 * focal_lengths[0]))); + //camera_features.view_angle_y = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getHeight(), (2.0 * focal_lengths[0]))); + float frac_x = ((float)active_size.width())/(float)pixel_size.getWidth(); + float frac_y = ((float)active_size.height())/(float)pixel_size.getHeight(); + float view_angle_x = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getWidth() * frac_x, (2.0 * focal_lengths[0]))); + float view_angle_y = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getHeight() * frac_y, (2.0 * focal_lengths[0]))); + if( MyDebug.LOG ) { + Log.d(TAG, "frac_x: " + frac_x); + Log.d(TAG, "frac_y: " + frac_y); + Log.d(TAG, "view_angle_x: " + view_angle_x); + Log.d(TAG, "view_angle_y: " + view_angle_y); + } + return new SizeF(view_angle_x, view_angle_y); } /* Returns true if the device supports the required hardware level, or better. diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java index d4becefb8bbfd5b0e0b61d400c2142573f19323b..d7a3f4ce8d42bf70f20c06e0fe76cfbbba58ba93 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java @@ -9,9 +9,12 @@ import android.content.Context; import android.graphics.Canvas; import android.location.Location; import android.net.Uri; +import android.util.Log; import android.util.Pair; import android.view.MotionEvent; +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.cameracontroller.CameraController; import net.sourceforge.opencamera.cameracontroller.RawImage; /** Provides communication between the Preview and the rest of the application @@ -56,7 +59,46 @@ public interface ApplicationInterface { String getCameraNoiseReductionModePref(); // CameraController.NOISE_REDUCTION_MODE_DEFAULT for device default, or "off", "minimal", "fast", "high_quality" String getISOPref(); // "auto" for auto-ISO, otherwise a numerical value; see documentation for Preview.supportsISORange(). int getExposureCompensationPref(); // 0 for default - Pair getCameraResolutionPref(); // return null to let Preview choose size + + class CameraResolutionConstraints { + private static final String TAG = "CameraResConstraints"; + + public boolean has_max_mp; + public int max_mp; + + boolean hasConstraints() { + return has_max_mp; + } + + boolean satisfies(CameraController.Size size) { + if( this.has_max_mp && size.width * size.height > this.max_mp ) { + if( MyDebug.LOG ) + Log.d(TAG, "size index larger than max_mp: " + this.max_mp); + return false; + } + return true; + } + } + /** The resolution to use for photo mode. + * If the returned resolution is not supported by the device, or this method returns null, then + * the preview will choose a size, and then call setCameraResolutionPref() with the chosen + * size. + * If the returned resolution is supported by the device, setCameraResolutionPref() will be + * called with the returned resolution. + * Note that even if the device supports the resolution in general, the Preview may choose a + * different resolution in some circumstances: + * * A burst mode as been requested, but the resolution does not support burst. + * * A constraint has been set via constraints. + * In such cases, the resolution actually in use should be found by calling + * Preview.getCurrentPictureSize() rather than relying on the setCameraResolutionPref(). (The + * logic behind this is that if a resolution is not supported by the device at all, it's good + * practice to correct the preference stored in user settings; but this shouldn't be done if + * the resolution is changed for something more temporary such as enabling burst mode.) + * @param constraints Optional constraints that may be set. If the returned resolution does not + * satisfy these constraints, then the preview will choose the closest + * resolution that does. + */ + Pair getCameraResolutionPref(CameraResolutionConstraints constraints); // return null to let Preview choose size int getImageQualityPref(); // jpeg quality for taking photos; "90" is a recommended default boolean getFaceDetectionPref(); // whether to use face detection mode String getVideoQualityPref(); // should be one of Preview.getSupportedVideoQuality() (use Preview.getCamcorderProfile() or Preview.getCamcorderProfileDescription() for details); or return "" to let Preview choose quality @@ -66,8 +108,9 @@ public interface ApplicationInterface { String getVideoBitratePref(); // return "default" to let Preview choose String getVideoFPSPref(); // return "default" to let Preview choose; if getVideoCaptureRateFactor() returns a value other than 1.0, this is the capture fps; the resultant video's fps will be getVideoFPSPref()*getVideoCaptureRateFactor() float getVideoCaptureRateFactor(); // return 1.0f for standard operation, less than 1.0 for slow motion, more than 1.0 for timelapse; consider using a higher fps for slow motion, see getVideoFPSPref() - boolean useVideoLogProfile(); // whether to use a log profile for video mode - float getVideoLogProfileStrength(); // strength of the log profile for video mode, if useVideoLogProfile() returns true + CameraController.TonemapProfile getVideoTonemapProfile(); // tonemap profile to use for video mode + float getVideoLogProfileStrength(); // strength of the log profile for video mode, if getVideoTonemapProfile() returns TONEMAPPROFILE_LOG + float getVideoProfileGamma(); // gamma for video mode, if getVideoTonemapProfile() returns TONEMAPPROFILE_GAMMA long getVideoMaxDurationPref(); // time in ms after which to automatically stop video recording (return 0 for off) int getVideoRestartTimesPref(); // number of times to restart video recording after hitting max duration (return 0 for never auto-restarting) VideoMaxFileSize getVideoMaxFileSizePref() throws NoFreeStorageException; // see VideoMaxFileSize class for details @@ -110,7 +153,8 @@ public interface ApplicationInterface { NRMODE_NORMAL, NRMODE_LOW_LIGHT } - NRModePref getNRModePref(); // only relevant if getBurstForNoiseReduction() returns true + NRModePref getNRModePref(); // only relevant if getBurstForNoiseReduction() returns true; if this changes without reopening the preview's camera, call Preview.setupBurstMode() + float getAperturePref(); // get desired aperture (called if Preview.getSupportedApertures() returns non-null); return -1.0f for no preference boolean getOptimiseAEForDROPref(); // see CameraController doc for setOptimiseAEForDRO(). enum RawPref { RAWPREF_JPEG_ONLY, // JPEG only @@ -134,6 +178,8 @@ public interface ApplicationInterface { void startedVideo(); // called just after video recording starts void stoppingVideo(); // called just before video recording stops; note that if startingVideo() is called but then video recording fails to start, this method will still be called, but startedVideo() and stoppedVideo() won't be called void stoppedVideo(final int video_method, final Uri uri, final String filename); // called after video recording stopped (uri/filename will be null if video is corrupt or not created); will be called iff startedVideo() was called + void restartedVideo(final int video_method, final Uri uri, final String filename); // called after a seamless restart (supported on Android 8+) has occurred - in this case stoppedVideo() is only called for the final video file; this method is instead called for all earlier video file segments + void deleteUnusedVideo(final int video_method, final Uri uri, final String filename); // application should delete the requested video (which will correspond to a video file previously returned via the createOutputVideo*() methods), either because it is corrupt or unused void onFailedStartPreview(); // called if failed to start camera preview void onCameraError(); // called if the camera closes due to serious error. void onPhotoError(); // callback for failing to take a photo @@ -172,6 +218,7 @@ public interface ApplicationInterface { void setVideoQualityPref(String video_quality); void setZoomPref(int zoom); void requestCameraPermission(); // for Android 6+: called when trying to open camera, but CAMERA permission not available + @SuppressWarnings("SameReturnValue") boolean needsStoragePermission(); // return true if the preview should call requestStoragePermission() if WRITE_EXTERNAL_STORAGE not available (i.e., if the application needs storage permission, e.g., to save photos) void requestStoragePermission(); // for Android 6+: called when trying to open camera, but WRITE_EXTERNAL_STORAGE permission not available void requestRecordAudioPermission(); // for Android 6+: called when switching to (or starting up in) video mode, but RECORD_AUDIO permission not available diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java index 7a2392949cb4c13ed88afacce134cf38093c1dbb..58395005f0ba191aca7fd2730ec57092dc69d885 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java @@ -91,7 +91,7 @@ public abstract class BasicApplicationInterface implements ApplicationInterface } @Override - public Pair getCameraResolutionPref() { + public Pair getCameraResolutionPref(CameraResolutionConstraints constraints) { return null; } @@ -141,8 +141,8 @@ public abstract class BasicApplicationInterface implements ApplicationInterface } @Override - public boolean useVideoLogProfile() { - return false; + public CameraController.TonemapProfile getVideoTonemapProfile() { + return CameraController.TonemapProfile.TONEMAPPROFILE_OFF; } @Override @@ -150,6 +150,11 @@ public abstract class BasicApplicationInterface implements ApplicationInterface return 0; } + @Override + public float getVideoProfileGamma() { + return 0; + } + @Override public long getVideoMaxDurationPref() { return 0; @@ -343,6 +348,11 @@ public abstract class BasicApplicationInterface implements ApplicationInterface return NRModePref.NRMODE_NORMAL; } + @Override + public float getAperturePref() { + return -1.0f; + } + @Override public boolean getOptimiseAEForDROPref() { return false; @@ -418,6 +428,14 @@ public abstract class BasicApplicationInterface implements ApplicationInterface } + @Override + public void restartedVideo(final int video_method, final Uri uri, final String filename) { + } + + @Override + public void deleteUnusedVideo(final int video_method, final Uri uri, final String filename) { + } + @Override public void onFailedStartPreview() { diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 7ecaec5c76538fb48ad008116ccf2aedce705755..2ac17bca7ba78fb840bc24a210ee7c2e256c0796 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -20,7 +20,6 @@ import net.sourceforge.opencamera.preview.camerasurface.MySurfaceView; import net.sourceforge.opencamera.preview.camerasurface.MyTextureView; import java.io.File; -import java.io.FileNotFoundException; //import java.io.FileOutputStream; import java.io.IOException; //import java.io.OutputStream; @@ -38,6 +37,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import android.Manifest; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; @@ -55,6 +55,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.SurfaceTexture; +import android.graphics.Typeface; import android.hardware.SensorEvent; import android.hardware.SensorManager; import android.location.Location; @@ -68,7 +69,6 @@ import android.os.Bundle; //import android.os.Environment; import android.os.Handler; import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSInvalidStateException; @@ -137,6 +137,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private boolean want_zebra_stripes; // whether to generate zebra stripes bitmap, requires want_preview_bitmap==true private int zebra_stripes_threshold; // pixels with max rgb value equal to or greater than this threshold are marked with zebra stripes + private int zebra_stripes_color_foreground; + private int zebra_stripes_color_background; private Bitmap zebra_stripes_bitmap_buffer; private Bitmap zebra_stripes_bitmap; @@ -195,6 +197,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } private VideoFileInfo videoFileInfo = new VideoFileInfo(); + private VideoFileInfo nextVideoFileInfo; // used for Android 8+ to handle seamless restart (see MediaRecorder.setNextOutputFile()) private static final int PHASE_NORMAL = 0; private static final int PHASE_TIMER = 1; @@ -220,11 +223,11 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private int current_orientation; // orientation received by onOrientationChanged private int current_rotation; // orientation relative to camera's orientation (used for parameters.setRotation()) private boolean has_level_angle; - private double natural_level_angle; // "level" angle of device, before applying any calibration and without accounting for screen orientation - private double level_angle; // "level" angle of device, including calibration - private double orig_level_angle; // "level" angle of device, including calibration, but without accounting for screen orientation + private double natural_level_angle; // "level" angle of device in degrees, before applying any calibration and without accounting for screen orientation + private double level_angle; // "level" angle of device in degrees, including calibration + private double orig_level_angle; // "level" angle of device in degrees, including calibration, but without accounting for screen orientation private boolean has_pitch_angle; - private double pitch_angle; + private double pitch_angle; // pitch angle of device in degrees // if applicationInterface.allowZoom() returns false, then has_zoom will be false, but camera_controller_supports_zoom // supports whether the camera controller supported zoom @@ -283,7 +286,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private List supported_preview_sizes; - private List sizes; + private List photo_sizes; + private ApplicationInterface.CameraResolutionConstraints photo_size_constraints; private int current_size_index = -1; // this is an index into the sizes array, or -1 if sizes not yet set private boolean supports_video; @@ -295,8 +299,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private Toast last_toast; private long last_toast_time_ms; - private final ToastBoxer flash_toast = new ToastBoxer(); - private final ToastBoxer focus_toast = new ToastBoxer(); + private final ToastBoxer focus_flash_toast = new ToastBoxer(); private final ToastBoxer take_photo_toast = new ToastBoxer(); private final ToastBoxer pause_video_toast = new ToastBoxer(); @@ -307,11 +310,13 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private CameraController.Face [] faces_detected; private final RectF face_rect = new RectF(); private final AccessibilityManager accessibility_manager; + private boolean supports_optical_stabilization; private boolean supports_video_stabilization; private boolean supports_photo_video_recording; private boolean can_disable_shutter_sound; private int tonemap_max_curve_points; private boolean supports_tonemap_curve; + private float [] supported_apertures; private boolean has_focus_area; private int focus_screen_x; private int focus_screen_y; @@ -337,11 +342,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private final float [] cameraRotation = new float[9]; private final float [] deviceInclination = new float[9]; private boolean has_geo_direction; - private final float [] geo_direction = new float[3]; + private final float [] geo_direction = new float[3]; // geo direction in radians private final float [] new_geo_direction = new float[3]; private final DecimalFormat decimal_format_1dp = new DecimalFormat("#.#"); - private final DecimalFormat decimal_format_2dp = new DecimalFormat("#.##"); + + // use use '0' instead of '#' to display e.g. 1.20 instead of 1.2, so that text lengths are consistent (e.g., for the + // toasts shown when changing sliders for manual focus distance or exposure compensation). + private final DecimalFormat decimal_format_2dp_force0 = new DecimalFormat("0.00"); /* If the user touches to focus in continuous mode, and in photo mode, we switch the camera_controller to autofocus mode. * autofocus_in_continuous_mode is set to true when this happens; the runnable reset_continuous_focus_runnable @@ -370,6 +378,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu public volatile boolean test_fail_open_camera; public volatile boolean test_video_failure; public volatile boolean test_ticker_called; // set from MySurfaceView or CanvasView + public volatile boolean test_called_next_output_file; + public volatile boolean test_started_next_output_file; + public volatile boolean test_runtime_on_video_stop; // force throwing a RuntimeException when stopping video (this usually happens naturally when stopping video too soon) + public volatile boolean test_burst_resolution; public Preview(ApplicationInterface applicationInterface, ViewGroup parent) { if( MyDebug.LOG ) { @@ -459,7 +471,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( !using_android_l ) { // see http://developer.android.com/reference/android/hardware/Camera.Face.html#rect // Need mirror for front camera - boolean mirror = camera_controller.isFrontFacing(); + boolean mirror = (camera_controller.getFacing() == CameraController.Facing.FACING_FRONT); camera_to_preview_matrix.setScale(mirror ? -1 : 1, 1); int display_orientation = camera_controller.getDisplayOrientation(); if( MyDebug.LOG ) { @@ -471,7 +483,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // Unfortunately the transformation for Android L API isn't documented, but this seems to work for Nexus 6. // This is the equivalent code for android.hardware.Camera.setDisplayOrientation, but we don't actually use setDisplayOrientation() // for CameraController2, except testing on Nexus 6 shows that we shouldn't change "result" for front facing camera. - boolean mirror = camera_controller.isFrontFacing(); + boolean mirror = (camera_controller.getFacing() == CameraController.Facing.FACING_FRONT); camera_to_preview_matrix.setScale(1, mirror ? -1 : 1); int degrees = getDisplayRotationDegrees(); int result = (camera_controller.getCameraOrientation() - degrees + 360) % 360; @@ -546,9 +558,18 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return areas; } + @SuppressWarnings("SameReturnValue") public boolean touchEvent(MotionEvent event) { if( MyDebug.LOG ) Log.d(TAG, "touch event at : " + event.getX() + " , " + event.getY() + " at time " + event.getEventTime()); + + // doesn't seem a bad idea to clear fake toasts (touching screen gets rid of standard toasts on Android 10+ at least) + this.clearActiveFakeToast(); + + boolean was_paused = !this.is_preview_started; + if( MyDebug.LOG ) + Log.d(TAG, "was_paused: " + was_paused); + if( gestureDetector.onTouchEvent(event) ) { if( MyDebug.LOG ) Log.d(TAG, "touch event handled by gestureDetector"); @@ -619,7 +640,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } cancelAutoFocus(); - if( camera_controller != null && !this.using_face_detection ) { + // don't set focus areas on touch if the user is touching to unpause! + if( camera_controller != null && !this.using_face_detection && !was_paused ) { this.has_focus_area = false; ArrayList areas = getAreas(event.getX(), event.getY()); if( camera_controller.setFocusAndMeteringArea(areas) ) { @@ -636,7 +658,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } - if( !this.is_video && applicationInterface.getTouchCapturePref() ) { + // don't take a photo on touch if the user is touching to unpause! + if( !this.is_video && !was_paused && applicationInterface.getTouchCapturePref() ) { if( MyDebug.LOG ) Log.d(TAG, "touch to capture"); // interpret as if user had clicked take photo/video button, except that we set the focus/metering areas @@ -644,7 +667,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return true; } - tryAutoFocus(false, true); + // don't auto focus on touch if the user is touching to unpause! + if( !was_paused ) { + tryAutoFocus(false, true); + } return true; } @@ -662,6 +688,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } + @SuppressWarnings("SameReturnValue") public boolean onDoubleTap() { if( MyDebug.LOG ) Log.d(TAG, "onDoubleTap()"); @@ -768,6 +795,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( camera_controller == null ) { if( MyDebug.LOG ) Log.d(TAG, "camera not opened!"); + //noinspection UnnecessaryReturnStatement return; } } @@ -825,9 +853,32 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture arg0, int width, int height) { - if( MyDebug.LOG ) + public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) { + if( MyDebug.LOG ) { Log.d(TAG, "onSurfaceTextureSizeChanged " + width + ", " + height); + //Log.d(TAG, "surface texture is now: " + ((TextureView)cameraSurface).getSurfaceTexture()); + } + + if( camera_controller != null ) { + camera_controller.test_texture_view_buffer_w = width; + camera_controller.test_texture_view_buffer_h = height; + + if( set_preview_size && (width != preview_w || height != preview_h) ) { + if( MyDebug.LOG ) + Log.d(TAG, "updatePreviewTexture"); + // Needed to fix problem if Open Camera is already running, and the aspect ratio changes (e.g., + // change of resolution, or switching between photo and video mode). When starting up in a "default", + // aspect ratio, the camera is opened via onSurfaceTextureAvailable(), and although we then call setAspectRatio(), + // there are no calls to onSurfaceTextureSizeChanged(). But when already running, or if + // an aspect ratio change for the view is required, changing the aspect ratio causes a call to + // onSurfaceTextureSizeChanged(), which results in the texture view's surface texture's buffer size being reset! + // (This can be seen in the source code of TextureView: onSizeChanged() calls setDefaultBufferSize() before + // calling onSurfaceTextureSizeChanged()!) So we need to call setDefaultBufferSize() again to reset + // to the desired preview buffer size that we already chose! + camera_controller.updatePreviewTexture(); + } + } + this.set_textureview_size = true; this.textureview_w = width; this.textureview_h = height; @@ -902,6 +953,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu try { if( MyDebug.LOG ) Log.d(TAG, "about to call video_recorder.stop()"); + if( test_runtime_on_video_stop ) + throw new RuntimeException(); video_recorder.stop(); if( MyDebug.LOG ) Log.d(TAG, "done video_recorder.stop()"); @@ -910,36 +963,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // stop() can throw a RuntimeException if stop is called too soon after start - this indicates the video file is corrupt, and should be deleted if( MyDebug.LOG ) Log.d(TAG, "runtime exception when stopping video"); - if( videoFileInfo.video_method == ApplicationInterface.VIDEOMETHOD_SAF ) { - if( videoFileInfo.video_uri != null ) { - if( MyDebug.LOG ) - Log.d(TAG, "delete corrupt video: " + videoFileInfo.video_uri); - try { - DocumentsContract.deleteDocument(getContext().getContentResolver(), videoFileInfo.video_uri); - } - catch(FileNotFoundException e2) { - // note, Android Studio reports a warning that FileNotFoundException isn't thrown, but it can be - // thrown by DocumentsContract.deleteDocument - and we get an error if we try to remove the catch! - if( MyDebug.LOG ) - Log.e(TAG, "exception when deleting " + videoFileInfo.video_uri); - e2.printStackTrace(); - } - } - } - else if( videoFileInfo.video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { - if( videoFileInfo.video_filename != null ) { - if( MyDebug.LOG ) - Log.d(TAG, "delete corrupt video: " + videoFileInfo.video_filename); - File file = new File(videoFileInfo.video_filename); - if( !file.delete() ) { - if( MyDebug.LOG ) - Log.e(TAG, "failed to delete corrupt video: " + videoFileInfo.video_filename); - } - } - } - // else don't delete if a plain Uri + applicationInterface.deleteUnusedVideo(videoFileInfo.video_method, videoFileInfo.video_uri, videoFileInfo.video_filename); videoFileInfo = new VideoFileInfo(); + nextVideoFileInfo = null; // if video recording is stopped quickly after starting, it's normal that we might not have saved a valid file, so no need to display a message if( !video_start_time_set || System.currentTimeMillis() - video_start_time > 2000 ) { VideoProfile profile = getVideoProfile(); @@ -962,7 +989,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu applicationInterface.cameraInOperation(false, true); reconnectCamera(false); // n.b., if something went wrong with video, then we reopen the camera - which may fail (or simply not reopen, e.g., if app is now paused) applicationInterface.stoppedVideo(videoFileInfo.video_method, videoFileInfo.video_uri, videoFileInfo.video_filename); + if( nextVideoFileInfo != null ) { + // if nextVideoFileInfo is not-null, it means we received MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING but not + // MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED, so it is the application responsibility to create the zero-size + // video file that will have been created + applicationInterface.deleteUnusedVideo(nextVideoFileInfo.video_method, nextVideoFileInfo.video_uri, nextVideoFileInfo.video_filename); + } videoFileInfo = new VideoFileInfo(); + nextVideoFileInfo = null; } private Context getContext() { @@ -1328,6 +1362,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu faces_detected = null; supports_face_detection = false; using_face_detection = false; + supports_optical_stabilization = false; supports_video_stabilization = false; supports_photo_video_recording = false; can_disable_shutter_sound = false; @@ -1359,8 +1394,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu supports_raw = false; view_angle_x = 55.0f; // set a sensible default view_angle_y = 43.0f; // set a sensible default - sizes = null; + photo_sizes = null; current_size_index = -1; + photo_size_constraints = null; has_capture_rate_factor = false; capture_rate_factor = 1.0f; video_high_speed = false; @@ -1540,7 +1576,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } }; - if( using_android_l ) { + if( using_android_l && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { + // n.b., using_android_l should only be set if Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP, + // but Android inspection warnings aren't clever enough to figure that out, and would otherwise + // complain about use of CameraController2 CameraController.ErrorCallback previewErrorCallback = new CameraController.ErrorCallback() { public void onError() { if( MyDebug.LOG ) @@ -1578,11 +1617,11 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "cameraOpened()"); debug_time = System.currentTimeMillis(); } - boolean take_photo = false; if( camera_controller != null ) { Activity activity = (Activity)Preview.this.getContext(); - if( MyDebug.LOG ) + /*if( MyDebug.LOG ) Log.d(TAG, "intent: " + activity.getIntent()); + boolean take_photo = false; if( activity.getIntent() != null && activity.getIntent().getExtras() != null ) { take_photo = activity.getIntent().getExtras().getBoolean(TakePhoto.TAKE_PHOTO); activity.getIntent().removeExtra(TakePhoto.TAKE_PHOTO); @@ -1590,7 +1629,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu else { if( MyDebug.LOG ) Log.d(TAG, "no intent data"); - } + }*/ + boolean take_photo = TakePhoto.TAKE_PHOTO; + if( take_photo ) + TakePhoto.TAKE_PHOTO = false; if( MyDebug.LOG ) Log.d(TAG, "take_photo?: " + take_photo); @@ -1725,7 +1767,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu this.updateFocusForVideo(); try { - setupCameraParameters(); + initCameraParameters(); } catch(CameraControllerException e) { e.printStackTrace(); @@ -1744,12 +1786,15 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "but video not supported"); saved_is_video = false; } - // must switch video before starting preview + // must switch video before setupCameraParameters(), and starting preview if( saved_is_video != this.is_video ) { if( MyDebug.LOG ) Log.d(TAG, "switch video mode as not in correct mode"); this.switchVideo(true, false); } + + setupCameraParameters(); + updateFlashForVideo(); if( take_photo ) { if( this.is_video ) { @@ -1763,18 +1808,24 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( MyDebug.LOG ) Log.d(TAG, "is_video?: " + is_video); if( this.is_video ) { - boolean use_video_log_profile = supports_tonemap_curve && applicationInterface.useVideoLogProfile(); - float video_log_profile_strength = use_video_log_profile ? applicationInterface.getVideoLogProfileStrength() : 0.0f; + CameraController.TonemapProfile tonemap_profile = CameraController.TonemapProfile.TONEMAPPROFILE_OFF; + if( supports_tonemap_curve ) { + tonemap_profile = applicationInterface.getVideoTonemapProfile(); + + } + float video_log_profile_strength = (tonemap_profile == CameraController.TonemapProfile.TONEMAPPROFILE_LOG) ? applicationInterface.getVideoLogProfileStrength() : 0.0f; + float video_gamma = (tonemap_profile == CameraController.TonemapProfile.TONEMAPPROFILE_GAMMA) ? applicationInterface.getVideoProfileGamma() : 0.0f; if( MyDebug.LOG ) { - Log.d(TAG, "use_video_log_profile: " + use_video_log_profile); + Log.d(TAG, "tonemap_profile: " + tonemap_profile); Log.d(TAG, "video_log_profile_strength: " + video_log_profile_strength); + Log.d(TAG, "video_gamma: " + video_gamma); } - camera_controller.setLogProfile(use_video_log_profile, video_log_profile_strength); + camera_controller.setTonemapProfile(tonemap_profile, video_log_profile_strength, video_gamma); } - // in theory it shouldn't matter if we call setVideoHighSpeed(true) if is_video==false, as it should only have an effect - // in video mode; but don't set high speed mode in photo mode just to be safe - // Setup for high speed - must be done after setupCameraParameters() and switching to video mode, but before setPreviewSize() and startCameraPreview() + // Setup for high speed - must be done after setupCameraParameters() and switching to video mode, but before setPreviewSize() and startCameraPreview(). + // In theory it shouldn't matter if we call setVideoHighSpeed(true) if is_video==false, as it should only have an effect + // when recording video; but don't set high speed mode in photo mode just to be safe. camera_controller.setVideoHighSpeed(is_video && video_high_speed); if( do_startup_focus && using_android_l && camera_controller.supportsAutoFocus() ) { @@ -1801,15 +1852,19 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu setupBurstMode(); if( camera_controller.isBurstOrExpo() ) { - // check photo resolution supports burst + if( MyDebug.LOG ) + Log.d(TAG, "check photo resolution supports burst"); CameraController.Size current_size = getCurrentPictureSize(); + if( MyDebug.LOG && current_size != null ) { + Log.d(TAG, "current_size: " + current_size.width + " x " + current_size.height + " supports_burst? " + current_size.supports_burst); + } if( current_size != null && !current_size.supports_burst ) { if( MyDebug.LOG ) Log.d(TAG, "burst mode: current picture size doesn't support burst"); // set to next largest that supports burst CameraController.Size new_size = null; - for(int i=0;i new_size.width*new_size.height ) { current_size_index = i; @@ -1820,8 +1875,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( new_size == null ) { Log.e(TAG, "can't find burst-supporting picture size smaller than the current picture size"); // just find largest that supports burst - for(int i=0;i new_size.width*new_size.height ) { current_size_index = i; @@ -1952,13 +2007,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } - private void setupCameraParameters() throws CameraControllerException { + private void initCameraParameters() throws CameraControllerException { if( MyDebug.LOG ) - Log.d(TAG, "setupCameraParameters()"); - long debug_time = 0; - if( MyDebug.LOG ) { - debug_time = System.currentTimeMillis(); - } + Log.d(TAG, "initCameraParameters()"); { // get available scene modes // important, from old Camera API docs: @@ -1984,9 +2035,6 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu applicationInterface.clearSceneModePref(); } } - if( MyDebug.LOG ) { - Log.d(TAG, "setupCameraParameters: time after setting scene mode: " + (System.currentTimeMillis() - debug_time)); - } { // grab all read-only info from parameters @@ -2005,17 +2053,31 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } this.minimum_focus_distance = camera_features.minimum_focus_distance; this.supports_face_detection = camera_features.supports_face_detection; - this.sizes = camera_features.picture_sizes; + this.photo_sizes = camera_features.picture_sizes; + if( test_burst_resolution ) { + // this flag means we pretend the largest resolution doesn't support burst + CameraController.Size current_size = null; + for(int i=0;i current_size.width*current_size.height ) { + current_size = size; + } + } + if( current_size != null ) + current_size.supports_burst = false; + } supported_flash_values = camera_features.supported_flash_values; supported_focus_values = camera_features.supported_focus_values; this.max_num_focus_areas = camera_features.max_num_focus_areas; this.is_exposure_lock_supported = camera_features.is_exposure_lock_supported; this.is_white_balance_lock_supported = camera_features.is_white_balance_lock_supported; + this.supports_optical_stabilization = camera_features.is_optical_stabilization_supported; this.supports_video_stabilization = camera_features.is_video_stabilization_supported; this.supports_photo_video_recording = camera_features.is_photo_video_recording_supported; this.can_disable_shutter_sound = camera_features.can_disable_shutter_sound; this.tonemap_max_curve_points = camera_features.tonemap_max_curve_points; this.supports_tonemap_curve = camera_features.supports_tonemap_curve; + this.supported_apertures = camera_features.apertures; this.supports_white_balance_temperature = camera_features.supports_white_balance_temperature; this.min_temperature = camera_features.min_temperature; this.max_temperature = camera_features.max_temperature; @@ -2040,8 +2102,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu this.video_quality_handler.setVideoSizesHighSpeed(camera_features.video_sizes_high_speed); this.supported_preview_sizes = camera_features.preview_sizes; } + } + + private void setupCameraParameters() { + if( MyDebug.LOG ) + Log.d(TAG, "setupCameraParameters()"); + long debug_time = 0; if( MyDebug.LOG ) { - Log.d(TAG, "setupCameraParameters: time after getting read only info: " + (System.currentTimeMillis() - debug_time)); + debug_time = System.currentTimeMillis(); } { @@ -2061,9 +2129,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } if( this.using_face_detection ) { class MyFaceDetectionListener implements CameraController.FaceDetectionListener { - final Handler handler = new Handler(); - int last_n_faces = -1; - FaceLocation last_face_location = FaceLocation.FACELOCATION_UNSET; + private final Handler handler = new Handler(); + private int last_n_faces = -1; + private FaceLocation last_face_location = FaceLocation.FACELOCATION_UNSET; /** Note, at least for Camera2 API, onFaceDetection() isn't called on UI thread. */ @@ -2152,6 +2220,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu break; case 90: { float temp = avg_x; + //noinspection SuspiciousNameCombination avg_x = avg_y; avg_y = 1.0f-temp; break; @@ -2239,10 +2308,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } { - if( MyDebug.LOG ) + if( MyDebug.LOG ) { Log.d(TAG, "set up video stabilization"); + Log.d(TAG, "is_video?: " + is_video); + } if( this.supports_video_stabilization ) { - boolean using_video_stabilization = applicationInterface.getVideoStabilizationPref(); + boolean using_video_stabilization = is_video && applicationInterface.getVideoStabilizationPref(); if( MyDebug.LOG ) Log.d(TAG, "using_video_stabilization?: " + using_video_stabilization); camera_controller.setVideoStabilization(using_video_stabilization); @@ -2350,7 +2421,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } - // must be done before setting flash modes, as we may remove flash modes if in manual mode + // must be done before setting flash modes, as we may remove flash modes if in manual mode (update: we now support flash for manual ISO anyway) if( MyDebug.LOG ) Log.d(TAG, "set up iso"); String value = applicationInterface.getISOPref(); @@ -2423,12 +2494,29 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu applicationInterface.clearExposureTimePref(); } - if( this.using_android_l && supported_flash_values != null ) { + if( supported_flash_values != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "restrict flash modes for manual mode"); + List new_supported_flash_values = new ArrayList<>(); + for(String supported_flash_value : supported_flash_values) { + switch( supported_flash_value ) { + case "flash_off": + case "flash_on": + case "flash_torch": + case "flash_frontscreen_on": + case "flash_frontscreen_torch": + new_supported_flash_values.add(supported_flash_value); + break; + } + } + supported_flash_values = new_supported_flash_values; + /* // flash modes not supported when using Camera2 and manual ISO // (it's unclear flash is useful - ideally we'd at least offer torch, but ISO seems to reset to 100 when flash/torch is on!) supported_flash_values = null; if( MyDebug.LOG ) Log.d(TAG, "flash not supported in Camera2 manual mode"); + */ } } if( MyDebug.LOG ) { @@ -2475,23 +2563,38 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "setupCameraParameters: time after exposures: " + (System.currentTimeMillis() - debug_time)); } + if( supported_apertures != null ) { + // set up aperture + float aperture = applicationInterface.getAperturePref(); + if( aperture > 0.0f ) { + // check supported + for(float this_aperture : supported_apertures) { + if( this_aperture == aperture ) { + camera_controller.setAperture(aperture); + } + } + // else don't set any aperture (leave as the device default) + } + } + { if( MyDebug.LOG ) Log.d(TAG, "set up picture sizes"); if( MyDebug.LOG ) { - for(int i=0;i resolution = applicationInterface.getCameraResolutionPref(); + photo_size_constraints = new ApplicationInterface.CameraResolutionConstraints(); + Pair resolution = applicationInterface.getCameraResolutionPref(photo_size_constraints); if( resolution != null ) { int resolution_w = resolution.first; int resolution_h = resolution.second; // now find size in valid list - for(int i=0;i current_size.width*current_size.height ) { current_size_index = i; current_size = size; @@ -2523,8 +2626,43 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // now save, so it's available for PreferenceActivity applicationInterface.setCameraResolutionPref(current_size.width, current_size.height); + + // check against constraints + // we intentionally do this after calling applicationInterface.setCameraResolutionPref() (as the constraints are + // used to just temporarily change resolution, e.g., if a maximum resolution has been enforced for HDR or NR photo + // mode, but we don't want to update the saved resolution preference in such cases + if( !photo_size_constraints.satisfies(current_size) ) { + if( MyDebug.LOG ) + Log.d(TAG, "current size index fail to satisfy constraints"); + CameraController.Size new_size = null; + // find the largest size that satisfies the constraint + for(int i=0;i new_size.width*new_size.height ) { + current_size_index = i; + new_size = size; + } + } + } + if( new_size == null ) { + Log.e(TAG, "can't find picture size that satisfies the constraints!"); + // so just choose the smallest + for(int i=0;i 1.0 ) { - // resultant framerate remains the same, instead adjst the capture rate + // resultant framerate remains the same, instead adjust the capture rate video_profile.videoCaptureRate = video_profile.videoCaptureRate / (double)capture_rate_factor; if( MyDebug.LOG ) Log.d(TAG, "scaled capture rate to: " + video_profile.videoCaptureRate); @@ -3246,7 +3387,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return width + ":" + height; } - private static String getMPString(int width, int height) { + public static String getMPString(int width, int height) { float mp = (width*height)/1000000.0f; return formatFloatToString(mp) + "MP"; } @@ -3260,49 +3401,52 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return "(" + getAspectRatio(width, height) + ", " + getMPString(width, height) + getBurstString(resources, supports_burst) + ")"; } - public String getCamcorderProfileDescriptionShort(String quality) { - if( camera_controller == null ) - return ""; - CamcorderProfile profile = getCamcorderProfile(quality); - // don't display MP here, as call to Preview.getMPString() here would contribute to poor performance (in PopupView)! - // this is meant to be a quick simple string - return profile.videoFrameWidth + "x" + profile.videoFrameHeight; - } - - public String getCamcorderProfileDescription(String quality) { - if( camera_controller == null ) - return ""; - CamcorderProfile profile = getCamcorderProfile(quality); - String highest = ""; - if( profile.quality == CamcorderProfile.QUALITY_HIGH ) { - highest = "Highest: "; - } + private String getCamcorderProfileDescriptionType(CamcorderProfile profile) { String type = ""; + // keep strings short, as displayed on the PopupView if( profile.videoFrameWidth == 3840 && profile.videoFrameHeight == 2160 ) { - type = "4K Ultra HD "; + type = "4K"; } else if( profile.videoFrameWidth == 1920 && profile.videoFrameHeight == 1080 ) { - type = "Full HD "; + type = "FullHD"; } else if( profile.videoFrameWidth == 1280 && profile.videoFrameHeight == 720 ) { - type = "HD "; + type = "HD"; } else if( profile.videoFrameWidth == 720 && profile.videoFrameHeight == 480 ) { - type = "SD "; + type = "SD"; } else if( profile.videoFrameWidth == 640 && profile.videoFrameHeight == 480 ) { - type = "VGA "; + type = "VGA"; } else if( profile.videoFrameWidth == 352 && profile.videoFrameHeight == 288 ) { - type = "CIF "; + type = "CIF"; } else if( profile.videoFrameWidth == 320 && profile.videoFrameHeight == 240 ) { - type = "QVGA "; + type = "QVGA"; } else if( profile.videoFrameWidth == 176 && profile.videoFrameHeight == 144 ) { - type = "QCIF "; + type = "QCIF"; } - return highest + type + profile.videoFrameWidth + "x" + profile.videoFrameHeight + " " + getAspectRatioMPString(getResources(), profile.videoFrameWidth, profile.videoFrameHeight, true); + return type; + } + + public String getCamcorderProfileDescriptionShort(String quality) { + if( camera_controller == null ) + return ""; + CamcorderProfile profile = getCamcorderProfile(quality); + String type = getCamcorderProfileDescriptionType(profile); + String space = type.length() == 0 ? "" : " "; + return profile.videoFrameWidth + "x" + profile.videoFrameHeight + space + type; + } + + public String getCamcorderProfileDescription(String quality) { + if( camera_controller == null ) + return ""; + CamcorderProfile profile = getCamcorderProfile(quality); + String type = getCamcorderProfileDescriptionType(profile); + String space = type.length() == 0 ? "" : " "; + return type + space + profile.videoFrameWidth + "x" + profile.videoFrameHeight + " " + getAspectRatioMPString(getResources(), profile.videoFrameWidth, profile.videoFrameHeight, true); } public double getTargetRatio() { @@ -3394,6 +3538,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // display size is returned in portrait format! (To reproduce, enable "Maximise preview size"; or if that's // already enabled, change the setting off and on.) if( display_size.x < display_size.y ) { + //noinspection SuspiciousNameCombination display_size.set(display_size.y, display_size.x); } if( MyDebug.LOG ) @@ -3592,7 +3737,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu this.current_orientation = orientation % 360; int new_rotation; int camera_orientation = camera_controller.getCameraOrientation(); - if( camera_controller.isFrontFacing() ) { + if( (camera_controller.getFacing() == CameraController.Facing.FACING_FRONT) ) { new_rotation = (camera_orientation - orientation + 360) % 360; } else { @@ -3635,7 +3780,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu int result; if( device_orientation == Configuration.ORIENTATION_PORTRAIT ) { // should be equivalent to onOrientationChanged(270) - if( camera_controller.isFrontFacing() ) { + if( (camera_controller.getFacing() == CameraController.Facing.FACING_FRONT) ) { result = (camera_orientation + 90) % 360; } else { @@ -3660,7 +3805,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } else { // should be equivalent to onOrientationChanged(90) - if( camera_controller.isFrontFacing() ) { + if( (camera_controller.getFacing() == CameraController.Facing.FACING_FRONT) ) { result = (camera_orientation + 270) % 360; } else { @@ -3810,7 +3955,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu String focus_distance_s; if( new_focus_distance > 0.0f ) { float real_focus_distance = 1.0f / new_focus_distance; - focus_distance_s = decimal_format_2dp.format(real_focus_distance) + getResources().getString(R.string.metres_abbreviation); + focus_distance_s = decimal_format_2dp_force0.format(real_focus_distance) + getResources().getString(R.string.metres_abbreviation); } else { focus_distance_s = getResources().getString(R.string.infinite); @@ -3922,7 +4067,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu public String getExposureCompensationString(int exposure) { float exposure_ev = exposure * exposure_step; - return getResources().getString(R.string.exposure_compensation) + " " + (exposure > 0 ? "+" : "") + decimal_format_2dp.format(exposure_ev) + " EV"; + // show a "+" even for exactly 0, so that we have a consistent text length (useful for the toast when adjusting the exposure compensation slider) + return getResources().getString(R.string.exposure_compensation) + " " + (exposure >= 0 ? "+" : "") + decimal_format_2dp_force0.format(exposure_ev) + " EV"; } public String getISOString(int iso) { @@ -4375,25 +4521,25 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu String features = ""; if( was_4k || was_bitrate || was_fps || was_slow_motion ) { if( was_4k ) { - features = "4K UHD"; + features = getContext().getResources().getString(R.string.error_features_4k); } if( was_bitrate ) { if( features.length() == 0 ) - features = "Bitrate"; + features = getContext().getResources().getString(R.string.error_features_bitrate); else - features += "/Bitrate"; + features += "/" + getContext().getResources().getString(R.string.error_features_bitrate); } if( was_fps ) { if( features.length() == 0 ) - features = "Frame rate"; + features = getContext().getResources().getString(R.string.error_features_frame_rate); else - features += "/Frame rate"; + features += "/" + getContext().getResources().getString(R.string.error_features_frame_rate); } if( was_slow_motion ) { if( features.length() == 0 ) - features = "Slow motion"; + features = getContext().getResources().getString(R.string.error_features_slow_motion); else - features += "/Slow motion"; + features += "/" + getContext().getResources().getString(R.string.error_features_slow_motion); } } return features; @@ -4431,12 +4577,39 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "cycleFlash()"); if( supported_flash_values != null ) { int new_flash_index = (current_flash_index+1) % supported_flash_values.size(); - if( supported_flash_values.get(new_flash_index).equals("flash_torch") ) { - if( MyDebug.LOG ) - Log.d(TAG, "cycle past torch"); - new_flash_index = (new_flash_index+1) % supported_flash_values.size(); + int start_index = new_flash_index; + boolean done = false; + while( !done ) { + done = true; + + if( skip_torch && supported_flash_values.get(new_flash_index).equals("flash_torch") ) { + if( MyDebug.LOG ) + Log.d(TAG, "cycle past torch"); + new_flash_index = (new_flash_index+1) % supported_flash_values.size(); + // don't bother setting done to false as we shouldn't have two torches in a row... + } + + if( is_video ) { + // check supported for video + String new_flash_value = supported_flash_values.get(new_flash_index); + if( !isFlashSupportedForVideo(new_flash_value) ) { + if( MyDebug.LOG ) + Log.d(TAG, "cycle past flash mode not supported for video: " + new_flash_value); + new_flash_index = (new_flash_index+1) % supported_flash_values.size(); + done = false; + } + } + + if( !done && new_flash_index == start_index ) { + // just in case, prevent infinite loop + Log.e(TAG, "flash looped to start - couldn't find valid flash!"); + break; + } + } + + if( done ) { + updateFlash(new_flash_index, save); } - updateFlash(new_flash_index, save); } } @@ -4464,7 +4637,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( MyDebug.LOG ) Log.d(TAG, " found entry: " + i); if( !initial ) { - showToast(flash_toast, flash_entries[i]); + showToast(focus_flash_toast, flash_entries[i]); } break; } @@ -4579,7 +4752,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( !quiet ) { String focus_entry = findFocusEntryForValue(focus_value); if( focus_entry != null ) { - showToast(focus_toast, focus_entry); + showToast(focus_flash_toast, focus_entry); } } this.setFocusValue(focus_value, auto_focus); @@ -4813,7 +4986,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "takePicturePressed exit"); } - private void takePictureOnTimer(final long timer_delay, boolean repeated) { + private void takePictureOnTimer(final long timer_delay, @SuppressWarnings("unused") boolean repeated) { if( MyDebug.LOG ) { Log.d(TAG, "takePictureOnTimer"); Log.d(TAG, "timer_delay: " + timer_delay); @@ -4849,7 +5022,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu takePictureTimer.schedule(takePictureTimerTask = new TakePictureTimerTask(), timer_delay); class BeepTimerTask extends TimerTask { - long remaining_time = timer_delay; + private long remaining_time = timer_delay; public void run() { if( remaining_time > 0 ) { // check in case this isn't cancelled by time we take the photo applicationInterface.timerBeep(remaining_time); @@ -4897,7 +5070,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "onVideoInfo: " + what + " extra: " + extra); if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING && video_restart_on_max_filesize ) { if( MyDebug.LOG ) - Log.d(TAG, "restart due to max filesize approaching - try setNextOutputFile"); + Log.d(TAG, "seamless restart due to max filesize approaching - try setNextOutputFile"); if( video_recorder == null ) { // just in case? if( MyDebug.LOG ) @@ -4953,7 +5126,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } if( MyDebug.LOG ) Log.d(TAG, "setNextOutputFile succeeded"); - videoFileInfo = info; + test_called_next_output_file = true; + nextVideoFileInfo = info; } catch(IOException e) { Log.e(TAG, "failed to setNextOutputFile"); @@ -4965,6 +5139,19 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // no need to explicitly stop if createVideoFile() or setNextOutputFile() fails - just let video reach max filesize // normally } + else if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && what == MediaRecorder.MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED && video_restart_on_max_filesize ) { + if( MyDebug.LOG ) + Log.d(TAG, "seamless restart with setNextOutputFile has now occurred"); + if( nextVideoFileInfo == null ) { + Log.e(TAG, "received MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED but nextVideoFileInfo is null"); + } + else { + applicationInterface.restartedVideo(videoFileInfo.video_method, videoFileInfo.video_uri, videoFileInfo.video_filename); + videoFileInfo = nextVideoFileInfo; + nextVideoFileInfo = null; + test_started_next_output_file = true; + } + } else if( what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED && video_restart_on_max_filesize ) { // note, if the restart was handled via MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING, then we shouldn't ever // receive MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED @@ -5136,6 +5323,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( MyDebug.LOG ) Log.d(TAG, "startVideoRecording"); focus_success = FOCUS_DONE; // clear focus rectangle (don't do for taking photos yet) + test_called_next_output_file = false; + test_started_next_output_file = false; + nextVideoFileInfo = null; final VideoProfile profile = getVideoProfile(); VideoFileInfo info = createVideoFile(profile.fileExtension); if( info == null ) { @@ -5201,9 +5391,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu boolean store_location = applicationInterface.getGeotaggingPref(); if( store_location && applicationInterface.getLocation() != null ) { Location location = applicationInterface.getLocation(); - if( MyDebug.LOG ) { - Log.d(TAG, "set video location: lat " + location.getLatitude() + " long " + location.getLongitude() + " accuracy " + location.getAccuracy()); - } + // don't log location, in case of privacy! local_video_recorder.setLocation((float)location.getLatitude(), (float)location.getLongitude()); } @@ -5759,6 +5947,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( pause_preview && success ) { if( is_preview_started ) { // need to manually stop preview on Android L Camera2 + // also note: even though we now draw the last image on top of the screen instead of relying on the + // camera preview being paused, it's still good practice to pause the preview/camera for privacy reasons if( camera_controller != null ) { camera_controller.stopPreview(); } @@ -6150,7 +6340,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } - boolean local_take_photo_after_autofocus = false; + boolean local_take_photo_after_autofocus; synchronized(this) { local_take_photo_after_autofocus = take_photo_after_autofocus; take_photo_after_autofocus = false; @@ -6260,6 +6450,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( this.natural_level_angle < -0.0 ) { this.natural_level_angle += 360.0; } + //natural_level_angle = 0.0f; // test zero angle updateLevelAngles(); } @@ -6306,14 +6497,20 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return this.has_level_angle; } + /** Returns the uncalibrated level angle in degrees. + */ public double getLevelAngleUncalibrated() { return this.natural_level_angle - this.current_orientation; } + /** Returns the level angle in degrees. + */ public double getLevelAngle() { return this.level_angle; } + /** Returns the original level angle in degrees. + */ public double getOrigLevelAngle() { return this.orig_level_angle; } @@ -6322,6 +6519,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return this.has_pitch_angle; } + /** Returns the pitch angle in degrees. + */ public double getPitchAngle() { return this.pitch_angle; } @@ -6409,6 +6608,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return has_geo_direction; } + /** Returns the geo direction in radians. + */ public double getGeoDirection() { return geo_direction[0]; } @@ -6418,12 +6619,44 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return supports_face_detection; } + /** Whether optical image stabilization (OIS) is supported by the device. + */ + public boolean supportsOpticalStabilization() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsOpticalStabilization"); + return supports_optical_stabilization; + } + + public boolean getOpticalStabilization() { + if( MyDebug.LOG ) + Log.d(TAG, "getOpticalStabilization"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return false; + } + return camera_controller.getOpticalStabilization(); + } + + /** Whether video digital stabilization is supported by the device. + */ public boolean supportsVideoStabilization() { if( MyDebug.LOG ) Log.d(TAG, "supportsVideoStabilization"); return supports_video_stabilization; } + public boolean getVideoStabilization() { + if( MyDebug.LOG ) + Log.d(TAG, "getVideoStabilization"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return false; + } + return camera_controller.getVideoStabilization(); + } + public boolean supportsPhotoVideoRecording() { if( MyDebug.LOG ) Log.d(TAG, "supportsPhotoVideoRecording"); @@ -6456,6 +6689,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return supports_tonemap_curve; } + /** Return the supported apertures for this camera. + */ + public float [] getSupportedApertures() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedApertures"); + return supported_apertures; + } + public List getSupportedColorEffects() { if( MyDebug.LOG ) Log.d(TAG, "getSupportedColorEffects"); @@ -6747,24 +6988,34 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } /** - * @param check_burst If true, and a burst mode is in use (fast burst, expo, HDR), then the - * returned list will be filtered to remove sizes that don't support burst. + * @param check_supported If true, and a burst mode is in use (fast burst, expo, HDR), and/or + * a constraint was set via getCameraResolutionPref(), then the returned + * list will be filtered to remove sizes that don't support burst and/or + * these constraints. */ - public List getSupportedPictureSizes(boolean check_burst) { + public List getSupportedPictureSizes(boolean check_supported) { if( MyDebug.LOG ) Log.d(TAG, "getSupportedPictureSizes"); - if( check_burst && camera_controller != null && camera_controller.isBurstOrExpo() ) { + boolean is_burst = ( camera_controller != null && camera_controller.isBurstOrExpo() ); + boolean has_constraints = photo_size_constraints != null && photo_size_constraints.hasConstraints(); + if( check_supported && ( is_burst || has_constraints ) ) { if( MyDebug.LOG ) - Log.d(TAG, "need to filter picture sizes for a burst mode"); + Log.d(TAG, "need to filter picture sizes for burst mode and/or constraints"); List filtered_sizes = new ArrayList<>(); - for(CameraController.Size size : sizes) { - if( size.supports_burst ) { + for(CameraController.Size size : photo_sizes) { + if( is_burst && !size.supports_burst ) { + // burst mode not supported + } + else if( !photo_size_constraints.satisfies(size) ) { + // doesn't satisfy imposed constraints + } + else { filtered_sizes.add(size); } } return filtered_sizes; } - return this.sizes; + return this.photo_sizes; } /*public int getCurrentPictureSizeIndex() { @@ -6774,9 +7025,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu }*/ public CameraController.Size getCurrentPictureSize() { - if( current_size_index == -1 || sizes == null ) + if( current_size_index == -1 || photo_sizes == null ) return null; - return sizes.get(current_size_index); + return photo_sizes.get(current_size_index); } public VideoQualityHandler getVideoQualityHander() { @@ -6879,6 +7130,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return supported_focus_values; } + /** Returns the current camera ID, or 0 if the camera isn't opened. + */ public int getCameraId() { if( camera_controller == null ) return 0; @@ -7010,12 +7263,19 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private final Rect bounds = new Rect(); private final Rect sub_bounds = new Rect(); private final RectF rect = new RectF(); + private final boolean style_outline; // if true, display text with outline rather than background - RotatedTextView(String text, int offset_y, Context context) { + RotatedTextView(String text, int offset_y, boolean style_outline, Context context) { super(context); this.lines = text.split("\n"); this.offset_y = offset_y; + this.style_outline = style_outline; + + if( style_outline ) { + // outline style looks clearer when using bold text + this.paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + } } void setText(String text) { @@ -7026,11 +7286,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu this.offset_y = offset_y; } + @SuppressLint("CanvasSize") @Override protected void onDraw(Canvas canvas) { final float scale = Preview.this.getResources().getDisplayMetrics().density; paint.setTextSize(14 * scale + 0.5f); // convert dps to pixels - paint.setShadowLayer(1, 0, 1, Color.BLACK); + if( !style_outline ) { + paint.setShadowLayer(1, 0, 1, Color.BLACK); + } //paint.getTextBounds(text, 0, text.length(), bounds); boolean first_line = true; for(String line : lines) { @@ -7072,15 +7335,30 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu rect.bottom = canvas.getHeight()/2.0f + bounds.bottom + padding + offset_y; paint.setStyle(Paint.Style.FILL); - paint.setColor(Color.rgb(50, 50, 50)); - //canvas.drawRect(rect, paint); - final float radius = (24 * scale + 0.5f); // convert dps to pixels - canvas.drawRoundRect(rect, radius, radius, paint); + if( !style_outline ) { + paint.setColor(Color.rgb(50, 50, 50)); + //paint.setColor(Color.argb(32, 0, 0, 0)); + //canvas.drawRect(rect, paint); + final float radius = (24 * scale + 0.5f); // convert dps to pixels + canvas.drawRoundRect(rect, radius, radius, paint); + } paint.setColor(Color.WHITE); int ypos = canvas.getHeight()/2 + offset_y - ((lines.length-1) * height)/2; for(String line : lines) { canvas.drawText(line, canvas.getWidth()/2.0f - bounds.width()/2.0f, ypos, paint); + + if( style_outline ) { + // draw outline + int current_color = paint.getColor(); + paint.setColor(Color.BLACK); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(1); + canvas.drawText(line, canvas.getWidth()/2.0f - bounds.width()/2.0f, ypos, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(current_color); + } + ypos += height; } canvas.restore(); @@ -7090,6 +7368,36 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private final Handler fake_toast_handler = new Handler(); private RotatedTextView active_fake_toast = null; + public void clearActiveFakeToast() { + clearActiveFakeToast(false); + } + + /** Removes any fake toast, if it exists. + * @param called_from_handler Should be false, unless called from the fake_toast_handler. + */ + private void clearActiveFakeToast(boolean called_from_handler) { + if( !called_from_handler ) { + // important to remove the callback, otherwise when it runs, it may end up deleting a + // new fake toast that is created after this method call, but before the callback runs + fake_toast_handler.removeCallbacksAndMessages(null); + } + // run on UI thread, to avoid threading issues + final Activity activity = (Activity)this.getContext(); + activity.runOnUiThread(new Runnable() { + public void run() { + if( active_fake_toast != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "remove fake toast: " + active_fake_toast); + ViewParent parent = active_fake_toast.getParent(); + if( parent != null ) { + ((ViewGroup)parent).removeView(active_fake_toast); + } + active_fake_toast = null; + } + } + }); + } + public void showToast(final ToastBoxer clear_toast, final int message_id) { showToast(clear_toast, getResources().getString(message_id), false); } @@ -7098,6 +7406,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu showToast(clear_toast, message, false); } + @SuppressWarnings("WeakerAccess") public void showToast(final String message, final boolean use_fake_toast) { showToast(null, message, use_fake_toast); } @@ -7106,7 +7415,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu showToast(clear_toast, message, 32, use_fake_toast); } - private void showToast(final String message, final int offset_y_dp, final boolean use_fake_toast) { + public void showToast(final String message, final int offset_y_dp, final boolean use_fake_toast) { showToast(null, message, offset_y_dp, use_fake_toast); } @@ -7126,46 +7435,67 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu * means that the toasts don't have the fade out effect. */ private void showToast(final ToastBoxer clear_toast, final String message, final int offset_y_dp, final boolean use_fake_toast) { + //final boolean use_fake_toast = true; + //final boolean use_fake_toast = old_use_fake_toast; if( !applicationInterface.getShowToastsPref() ) { return; } if( MyDebug.LOG ) Log.d(TAG, "showToast: " + message); + + if( this.app_is_paused ) { + if( MyDebug.LOG ) + Log.e(TAG, "don't show toast as application is paused: " + message); + // when targeting Android 11+, toasts with custom views won't be shown in background anyway - in theory we + // shouldn't be making toasts when in background, but check just in case + return; + } + final Activity activity = (Activity)this.getContext(); // We get a crash on emulator at least if Toast constructor isn't run on main thread (e.g., the toast for taking a photo when on timer). // Also see http://stackoverflow.com/questions/13267239/toast-from-a-non-ui-thread - // Also for the use_fake_toast code, running the creation code, and the postDelayed code, on the UI thread avoids threading issues + // Also for the use_fake_toast code, running the creation code, and the postDelayed code (and the code in clearActiveFakeToast()), on the UI thread avoids threading issues activity.runOnUiThread(new Runnable() { public void run() { + if( Preview.this.app_is_paused ) { + if( MyDebug.LOG ) + Log.e(TAG, "don't show toast as application is paused: " + message); + // see note above + return; + } + final float scale = Preview.this.getResources().getDisplayMetrics().density; final int offset_y = (int) (offset_y_dp * scale + 0.5f); // convert dps to pixels if( use_fake_toast ) { if( active_fake_toast != null ) { // re-use existing fake toast + if( MyDebug.LOG ) + Log.d(TAG, "re-use fake toast: " + active_fake_toast); active_fake_toast.setText(message); active_fake_toast.setOffsetY(offset_y); active_fake_toast.invalidate(); // make sure the view is redrawn - fake_toast_handler.removeCallbacksAndMessages(null); } else { - active_fake_toast = new RotatedTextView(message, offset_y, activity); + active_fake_toast = new RotatedTextView(message, offset_y, true, activity); + if( MyDebug.LOG ) + Log.d(TAG, "create new fake toast: " + active_fake_toast); Activity activity = (Activity) Preview.this.getContext(); final FrameLayout rootLayout = activity.findViewById(android.R.id.content); rootLayout.addView(active_fake_toast); } + // in theory the fake_toast_handler should only have a callback on it if re-using an existing fake toast, + // but we remove callbacks always just in case + fake_toast_handler.removeCallbacksAndMessages(null); + fake_toast_handler.postDelayed(new Runnable() { @Override public void run() { if( MyDebug.LOG ) - Log.d(TAG, "remove fake toast: " + active_fake_toast); - ViewParent parent = active_fake_toast.getParent(); - if( parent != null ) { - ((ViewGroup)parent).removeView(active_fake_toast); - } - active_fake_toast = null; + Log.d(TAG, "destroy fake toast due to time expired"); + clearActiveFakeToast(true); } }, 2000); // supposedly matches Toast.LENGTH_SHORT @@ -7213,7 +7543,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "created new toast: " + toast); if( clear_toast != null ) clear_toast.toast = toast; - View text = new RotatedTextView(message, offset_y, activity); + View text = new RotatedTextView(message, offset_y, false, activity); toast.setView(text); last_toast_time_ms = time_now; } @@ -7251,7 +7581,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Location location = applicationInterface.getLocation(); if( MyDebug.LOG ) { Log.d(TAG, "updating parameters from location..."); - Log.d(TAG, "lat " + location.getLatitude() + " long " + location.getLongitude() + " accuracy " + location.getAccuracy() + " timestamp " + location.getTime()); + // don't log location, in case of privacy! } camera_controller.setLocationInfo(location); } @@ -7352,6 +7682,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu int rotation = getDisplayRotationDegrees(); if( rotation == 90 || rotation == 270 ) { int dummy = bitmap_width; + //noinspection SuspiciousNameCombination bitmap_width = bitmap_height; bitmap_height = dummy; } @@ -7451,9 +7782,11 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return this.histogram; } - public void enableZebraStripes(int zebra_stripes_threshold) { + public void enableZebraStripes(int zebra_stripes_threshold, int zebra_stripes_color_foreground, int zebra_stripes_color_background) { this.want_zebra_stripes = true; this.zebra_stripes_threshold = zebra_stripes_threshold; + this.zebra_stripes_color_foreground = zebra_stripes_color_foreground; + this.zebra_stripes_color_background = zebra_stripes_color_background; if( this.zebra_stripes_bitmap_buffer == null ) { createZebraStripesBitmap(); } @@ -7693,6 +8026,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Allocation output_allocation = Allocation.createFromBitmap(preview.rs, zebra_stripes_bitmap_buffer); histogramScript.set_zebra_stripes_threshold(preview.zebra_stripes_threshold); + histogramScript.set_zebra_stripes_foreground_r(Color.red(preview.zebra_stripes_color_foreground)); + histogramScript.set_zebra_stripes_foreground_g(Color.green(preview.zebra_stripes_color_foreground)); + histogramScript.set_zebra_stripes_foreground_b(Color.blue(preview.zebra_stripes_color_foreground)); + histogramScript.set_zebra_stripes_foreground_a(Color.alpha(preview.zebra_stripes_color_foreground)); + histogramScript.set_zebra_stripes_background_r(Color.red(preview.zebra_stripes_color_background)); + histogramScript.set_zebra_stripes_background_g(Color.green(preview.zebra_stripes_color_background)); + histogramScript.set_zebra_stripes_background_b(Color.blue(preview.zebra_stripes_color_background)); + histogramScript.set_zebra_stripes_background_a(Color.alpha(preview.zebra_stripes_color_background)); histogramScript.set_zebra_stripes_width(zebra_stripes_bitmap_buffer.getWidth()/20); if( MyDebug.LOG ) @@ -7957,6 +8298,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return this.supported_focus_values != null; } + /** Whether flash is supported by the camera. + */ public boolean supportsFlash() { return this.supported_flash_values != null; } diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java b/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java index ec7b5fac8193e163996082fe8b320ad2a3069ba6..8ebe7c1cb5b84d7fd01ee4e8735bdbd820de6ea9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java @@ -17,7 +17,9 @@ public class VideoProfile { public int audioSource; public int audioCodec; public int audioChannels; + @SuppressWarnings("WeakerAccess") public int audioBitRate; + @SuppressWarnings("WeakerAccess") public int audioSampleRate; public int fileFormat; public String fileExtension = "mp4"; diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/camerasurface/MySurfaceView.java b/app/src/main/java/net/sourceforge/opencamera/preview/camerasurface/MySurfaceView.java index fcd1768f75b901308dce4e5f155f511f970e3739..2ea060e59a42eebea05b1e3fb911197894113d6d 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/camerasurface/MySurfaceView.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/camerasurface/MySurfaceView.java @@ -13,7 +13,6 @@ import android.media.MediaRecorder; import android.os.Handler; import android.util.Log; import android.view.MotionEvent; -import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; @@ -39,7 +38,7 @@ public class MySurfaceView extends SurfaceView implements CameraSurface { // underlying surface is created and destroyed. getHolder().addCallback(preview); // deprecated setting, but required on Android versions prior to 3.0 - getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // deprecated + //getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // deprecated tick = new Runnable() { public void run() { diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java index f2c6d53382efaeeebfa6097075dc806637744dde..b9a6f4b07bb422f4bf92abce6755c85a14ce1c41 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java @@ -38,15 +38,16 @@ public class BluetoothLeService extends Service { private String device_address; private BluetoothGatt bluetoothGatt; private String remote_device_type; + private final Handler bluetoothHandler = new Handler(); private final HashMap subscribed_characteristics = new HashMap<>(); private final List charsToSubscribe = new ArrayList<>(); private double currentTemp = -1; private double currentDepth = -1; - private static final int STATE_DISCONNECTED = 0; + /*private static final int STATE_DISCONNECTED = 0; private static final int STATE_CONNECTING = 1; - private static final int STATE_CONNECTED = 2; + private static final int STATE_CONNECTED = 2;*/ public final static String ACTION_GATT_CONNECTED = "net.sourceforge.opencamera.Remotecontrol.ACTION_GATT_CONNECTED"; @@ -73,6 +74,21 @@ public class BluetoothLeService extends Service { public final static int COMMAND_UP = 64; public final static int COMMAND_DOWN = 80; + /* This forces a gratuitous BLE scan to help the device + * connect to the remote faster. This is due to limitations of the + * Android BLE stack and API (just knowing the MAC is not enough on + * many phones).*/ + private void triggerScan() { + // Stops scanning after a pre-defined scan period. + bluetoothHandler.postDelayed(new Runnable() { + @Override + public void run() { + bluetoothAdapter.stopLeScan(null); + } + }, 10000); + bluetoothAdapter.startLeScan(null); + } + public void setRemoteDeviceType(String remote_device_type) { if( MyDebug.LOG ) Log.d(TAG, "Setting remote type: " + remote_device_type); @@ -160,6 +176,7 @@ public class BluetoothLeService extends Service { if (gattServices == null) return; List mCharacteristicsWanted; + //noinspection SwitchStatementWithTooFewBranches switch( remote_device_type ) { case "preference_remote_type_kraken": mCharacteristicsWanted = KrakenGattAttributes.getDesiredCharacteristics(); @@ -325,7 +342,7 @@ public class BluetoothLeService extends Service { return false; } - if( device_address != null && address.equals(device_address) && bluetoothGatt != null ) { + if( address.equals(device_address) && bluetoothGatt != null ) { bluetoothGatt.disconnect(); bluetoothGatt.close(); bluetoothGatt = null; @@ -346,7 +363,13 @@ public class BluetoothLeService extends Service { return false; } - bluetoothGatt = device.connectGatt(this, false, mGattCallback); + // It looks like Android won't connect to BLE devices properly without scanning + // for them first, even when connecting by explicit MAC address. Since we're using + // BLE for underwater housings and we want rock solid connectivity, we trigger + // a scan for 10 seconds + triggerScan(); + + bluetoothGatt = device.connectGatt(this, true, mGattCallback); device_address = address; return true; } diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java index b2938335c7c9a9c89a4636ae67940fdcf8191aa5..a1130e9dc9d7c774d1e6295e50d471576936b9ac 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java @@ -176,6 +176,7 @@ public class DeviceScanner extends ListActivity { @NonNull int[] grantResults) { if( MyDebug.LOG ) Log.d(TAG, "onRequestPermissionsResult: requestCode " + requestCode); + //noinspection SwitchStatementWithTooFewBranches switch (requestCode) { case REQUEST_LOCATION_PERMISSIONS: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/ArraySeekBarPreference.java b/app/src/main/java/net/sourceforge/opencamera/ui/ArraySeekBarPreference.java new file mode 100644 index 0000000000000000000000000000000000000000..1f06607497e3c18a9b4a5317cc3edb23916212d7 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ui/ArraySeekBarPreference.java @@ -0,0 +1,236 @@ +package net.sourceforge.opencamera.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.preference.DialogPreference; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import foundation.e.camera.R; + +/** This contains a custom preference to display a seekbar in place of a ListPreference. + */ +public class ArraySeekBarPreference extends DialogPreference { + //private static final String TAG = "ArraySeekBarPreference"; + + private SeekBar seekbar; + private TextView textView; + + private CharSequence [] entries; // user readable strings + private CharSequence [] values; // values corresponding to each string + + private final String default_value; + private String value; // current saved value of this preference (note that this is intentionally not updated when the seekbar changes, as we don't save until the user clicks ok) + private boolean value_set; + + public ArraySeekBarPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + String namespace = "http://schemas.android.com/apk/res/android"; + this.default_value = attrs.getAttributeValue(namespace, "defaultValue"); + + int entries_id = attrs.getAttributeResourceValue(namespace, "entries", 0); + if( entries_id > 0 ) + this.setEntries(entries_id); + int values_id = attrs.getAttributeResourceValue(namespace, "entryValues", 0); + if( values_id > 0 ) + this.setEntryValues(values_id); + + setDialogLayoutResource(R.layout.arrayseekbarpreference); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + if( entries == null || values == null ) { + throw new IllegalStateException("ArraySeekBarPreference requires entries and entryValues array"); + } + else if( entries.length != values.length ) { + throw new IllegalStateException("ArraySeekBarPreference requires entries and entryValues arrays of same length"); + } + + this.seekbar = view.findViewById(R.id.arrayseekbarpreference_seekbar); + this.textView = view.findViewById(R.id.arrayseekbarpreference_value); + + seekbar.setMax(entries.length-1); + { + int index = getValueIndex(); + if( index == -1 ) { + // If we're here, it means the stored value isn't in the values array. + // ListPreference just shows a dialog with no selected entry, but that doesn't really work for + // a seekbar that needs to show the current position! So instead, set the position to the default. + if( default_value != null && values != null ) { + for(int i = values.length - 1; i >= 0; i--) { + if( values[i].equals(default_value) ) { + index = i; + break; + } + } + } + } + if( index >= 0 ) + seekbar.setProgress(index); + } + seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + String new_entry = entries[progress].toString(); + textView.setText(new_entry); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + String new_entry = entries[seekbar.getProgress()].toString(); + textView.setText(new_entry); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if( positiveResult && values != null ) { + int progress = seekbar.getProgress(); + String new_value = values[progress].toString(); + if( callChangeListener(new_value) ) { + setValue(new_value); + } + } + } + + public void setEntries(CharSequence[] entries) { + this.entries = entries; + } + + private void setEntries(int entries) { + setEntries(getContext().getResources().getTextArray(entries)); + } + + public void setEntryValues(CharSequence[] values) { + this.values = values; + } + + private void setEntryValues(int values) { + setEntryValues(getContext().getResources().getTextArray(values)); + } + + @Override + public CharSequence getSummary() { + CharSequence summary = super.getSummary(); + if( summary != null ) { + CharSequence entry = getEntry(); + return String.format(summary.toString(), entry == null ? "" : entry); + } + else + return null; + } + + /** Returns the index of the current value in the values array, or -1 if not found. + */ + private int getValueIndex() { + if( value != null && values != null ) { + // go backwards for compatibility with ListPreference in cases with duplicate values + for(int i = values.length - 1; i >= 0; i--) { + if( values[i].equals(value) ) { + return i; + } + } + } + return -1; + } + + /** Returns the human readable string of the current value. + */ + private CharSequence getEntry() { + int index = getValueIndex(); + return index >= 0 && entries != null ? entries[index] : null; + } + + private void setValue(String value) { + final boolean changed = !TextUtils.equals(this.value, value); + if( changed || !value_set ) { + this.value = value; + value_set = true; + persistString(value); + if( changed ) { + notifyChanged(); + } + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValue(restoreValue ? getPersistedString(value) : (String) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if( isPersistent() ) { + return superState; + } + + final SavedState state = new SavedState(superState); + state.value = value; + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if( state == null || !state.getClass().equals(SavedState.class) ) { + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState)state; + super.onRestoreInstanceState(myState.getSuperState()); + setValue(myState.value); + } + + private static class SavedState extends BaseSavedState { + String value; + + SavedState(Parcel source) { + super(source); + value = source.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(value); + } + + SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java index 1b39710bac619fb8a2a1012b6a22ec47ee415aad..6577287fda276f969fd5a261a469e8241d9ce07c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Locale; import net.sourceforge.opencamera.GyroSensor; +import net.sourceforge.opencamera.ImageSaver; +import net.sourceforge.opencamera.LocationSupplier; import net.sourceforge.opencamera.MainActivity; import net.sourceforge.opencamera.MyApplicationInterface; import net.sourceforge.opencamera.MyDebug; @@ -32,21 +34,24 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; +import android.location.Location; import android.media.ExifInterface; import android.net.Uri; import android.os.BatteryManager; import android.os.Build; import android.preference.PreferenceManager; -import android.provider.MediaStore; import android.util.Log; import android.util.Pair; +import android.view.Display; import android.view.Surface; import android.view.View; +import android.widget.RelativeLayout; public class DrawPreview { private static final String TAG = "DrawPreview"; @@ -61,6 +66,7 @@ public class DrawPreview { private boolean has_settings; private MyApplicationInterface.PhotoMode photoMode; private boolean show_time_pref; + private boolean show_camera_id_pref; private boolean show_free_memory_pref; private boolean show_iso_pref; private boolean show_video_max_amp_pref; @@ -88,10 +94,13 @@ public class DrawPreview { private String ghost_image_pref; private String ghost_selected_image_pref = ""; private Bitmap ghost_selected_image_bitmap; + private int ghost_image_alpha; private boolean want_histogram; private Preview.HistogramType histogram_type; private boolean want_zebra_stripes; private int zebra_stripes_threshold; + private int zebra_stripes_color_foreground; + private int zebra_stripes_color_background; private boolean want_focus_peaking; private int focus_peaking_color_pref; @@ -103,11 +112,15 @@ public class DrawPreview { private final float scale; private final float stroke_width; // stroke_width used for various UI elements private Calendar calendar; - private final DateFormat dateFormatTimeInstance = DateFormat.getTimeInstance(); + private DateFormat dateFormatTimeInstance; private final String ybounds_text; private final int [] temp_histogram_channel = new int[256]; + private final LocationSupplier.LocationInfo locationInfo = new LocationSupplier.LocationInfo(); + private final int [] auto_stabilise_crop = new int [2]; + //private final DecimalFormat decimal_format_1dp_force0 = new DecimalFormat("0.0"); // cached Rects for drawTextWithBackground() calls private Rect text_bounds_time; + private Rect text_bounds_camera_id; private Rect text_bounds_free_memory; private Rect text_bounds_angle_single; private Rect text_bounds_angle_double; @@ -124,6 +137,9 @@ public class DrawPreview { private String current_time_string; private long last_current_time_time; + private String camera_id_string; + private long last_camera_id_time; + private String iso_exposure_string; private boolean is_scanning; private long last_iso_exposure_time; @@ -203,10 +219,21 @@ public class DrawPreview { private float view_angle_y_preview; private long last_view_angles_time; + private int take_photo_top; // coordinate (in canvas x coordinates) of top of the take photo icon + private long last_take_photo_top_time; + + private int top_icon_shift; // shift that may be needed for on-screen text to avoid clashing with icons (when arranged "along top") + private long last_top_icon_shift_time; + + private int focus_seekbars_margin_left = -1; // margin left that's been set for the focus seekbars + // OSD extra lines private String OSDLine1; private String OSDLine2; + private final static int histogram_width_dp = 100; + private final static int histogram_height_dp = 60; + public DrawPreview(MainActivity main_activity, MyApplicationInterface applicationInterface) { if( MyDebug.LOG ) Log.d(TAG, "DrawPreview"); @@ -348,6 +375,28 @@ public class DrawPreview { return main_activity; } + /** Computes the x coordinate on screen of left side of the view, equivalent to + * view.getLocationOnScreen(), but we undo the effect of the view's rotation. + * This is because getLocationOnScreen() will return the coordinates of the view's top-left + * *after* applying the rotation, when we want the top left of the icon as shown on screen. + * This should not be called every frame but instead should be cached, due to cost of calling + * view.getLocationOnScreen(). + */ + private int getViewOnScreenX(View view) { + view.getLocationOnScreen(gui_location); + int xpos = gui_location[0]; + int rotation = Math.round(view.getRotation()); + // rotation can be outside [0, 359] if the user repeatedly rotates in same direction! + rotation = (rotation % 360 + 360) % 360; // version of (rotation % 360) that work if rotation is -ve + /*if( MyDebug.LOG ) + Log.d(TAG, " mod rotation: " + rotation);*/ + if( rotation == 180 || rotation == 90 ) { + // annoying behaviour that getLocationOnScreen takes the rotation into account + xpos -= view.getWidth(); + } + return xpos; + } + /** Sets a current thumbnail for a photo or video just taken. Used for thumbnail animation, * and when ghosting the last image. */ @@ -472,6 +521,14 @@ public class DrawPreview { Log.d(TAG, "photoMode: " + photoMode); show_time_pref = sharedPreferences.getBoolean(PreferenceKeys.ShowTimePreferenceKey, true); + // reset in case user changes the preference: + dateFormatTimeInstance = DateFormat.getTimeInstance(); + current_time_string = null; + last_current_time_time = 0; + text_bounds_time = null; + + show_camera_id_pref = main_activity.isMultiCam() && sharedPreferences.getBoolean(PreferenceKeys.ShowCameraIDPreferenceKey, true); + //show_camera_id_pref = true; // test show_free_memory_pref = sharedPreferences.getBoolean(PreferenceKeys.ShowFreeMemoryPreferenceKey, true); show_iso_pref = sharedPreferences.getBoolean(PreferenceKeys.ShowISOPreferenceKey, true); show_video_max_amp_pref = sharedPreferences.getBoolean(PreferenceKeys.ShowVideoMaxAmpPreferenceKey, false); @@ -554,6 +611,7 @@ public class DrawPreview { } ghost_selected_image_pref = ""; } + ghost_image_alpha = applicationInterface.getGhostImageAlpha(); String histogram_pref = sharedPreferences.getString(PreferenceKeys.HistogramPreferenceKey, "preference_histogram_off"); want_histogram = !histogram_pref.equals("preference_histogram_off") && main_activity.supportsPreviewBitmaps(); @@ -590,12 +648,22 @@ public class DrawPreview { } want_zebra_stripes = zebra_stripes_threshold != 0 & main_activity.supportsPreviewBitmaps(); + String zebra_stripes_color_foreground_value = sharedPreferences.getString(PreferenceKeys.ZebraStripesForegroundColorPreferenceKey, "#ff000000"); + zebra_stripes_color_foreground = Color.parseColor(zebra_stripes_color_foreground_value); + String zebra_stripes_color_background_value = sharedPreferences.getString(PreferenceKeys.ZebraStripesBackgroundColorPreferenceKey, "#ffffffff"); + zebra_stripes_color_background = Color.parseColor(zebra_stripes_color_background_value); + String focus_peaking_pref = sharedPreferences.getString(PreferenceKeys.FocusPeakingPreferenceKey, "preference_focus_peaking_off"); want_focus_peaking = !focus_peaking_pref.equals("preference_focus_peaking_off") && main_activity.supportsPreviewBitmaps(); String focus_peaking_color = sharedPreferences.getString(PreferenceKeys.FocusPeakingColorPreferenceKey, "#ffffff"); focus_peaking_color_pref = Color.parseColor(focus_peaking_color); + last_camera_id_time = 0; // in case camera id changed last_view_angles_time = 0; // force view angles to be recomputed + last_take_photo_top_time = 0; // force take_photo_top to be recomputed + last_top_icon_shift_time = 0; // for top_icon_shift to be recomputed + + focus_seekbars_margin_left = -1; // just in case?? has_settings = true; } @@ -616,13 +684,60 @@ public class DrawPreview { /** Loads the bitmap from the uri. File is optional, and is used on pre-Android 7 devices to * read the exif orientation. + * The image will be downscaled if required to be comparable to the preview width. */ private Bitmap loadBitmap(Uri uri, File file) throws IOException { if( MyDebug.LOG ) Log.d(TAG, "loadBitmap: " + uri); Bitmap bitmap; try { - bitmap = MediaStore.Images.Media.getBitmap(main_activity.getContentResolver(), uri); + //bitmap = MediaStore.Images.Media.getBitmap(main_activity.getContentResolver(), uri); + + int sample_size = 1; + { + // attempt to compute appropriate scaling + BitmapFactory.Options bounds = new BitmapFactory.Options(); + bounds.inJustDecodeBounds = true; + InputStream input = main_activity.getContentResolver().openInputStream(uri); + BitmapFactory.decodeStream(input, null, bounds); + if( input != null ) + input.close(); + + if( bounds.outWidth != -1 && bounds.outHeight != -1 ) { + // compute appropriate scaling + int image_size = Math.max(bounds.outWidth, bounds.outHeight); + + Point point = new Point(); + Display display = main_activity.getWindowManager().getDefaultDisplay(); + display.getSize(point); + int display_size = Math.max(point.x, point.y); + + int ratio = (int) Math.ceil((double) image_size / display_size); + sample_size = Integer.highestOneBit(ratio); + if( MyDebug.LOG ) { + Log.d(TAG, "display_size: " + display_size); + Log.d(TAG, "image_size: " + image_size); + Log.d(TAG, "ratio: " + ratio); + Log.d(TAG, "sample_size: " + sample_size); + } + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "failed to obtain width/height of bitmap"); + } + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = false; + options.inSampleSize = sample_size; + InputStream input = main_activity.getContentResolver().openInputStream(uri); + bitmap = BitmapFactory.decodeStream(input, null, options); + if( input != null ) + input.close(); + if( MyDebug.LOG && bitmap != null ) { + Log.d(TAG, "bitmap width: " + bitmap.getWidth()); + Log.d(TAG, "bitmap height: " + bitmap.getHeight()); + } } catch(Exception e) { // Although Media.getBitmap() is documented as only throwing FileNotFoundException, IOException @@ -969,6 +1084,7 @@ public class DrawPreview { p.setTextAlign(Paint.Align.LEFT); int location_x = top_x; int location_y = top_y; + final int gap_x = (int) (8 * scale + 0.5f); // convert dps to pixels final int gap_y = (int) (0 * scale + 0.5f); // convert dps to pixels final int icon_gap_y = (int) (2 * scale + 0.5f); // convert dps to pixels if( ui_rotation == 90 || ui_rotation == 270 ) { @@ -979,11 +1095,15 @@ public class DrawPreview { if( ui_rotation == 90 ) { location_y = canvas.getHeight() - location_y - (int) (20 * scale + 0.5f); } + boolean align_right = false; if( ui_rotation == 180 ) { location_x = canvas.getWidth() - location_x; p.setTextAlign(Paint.Align.RIGHT); + align_right = true; } + int first_line_height = 0; + int first_line_xshift = 0; if( show_time_pref ) { if( current_time_string == null || time_ms/1000 > last_current_time_time/1000 ) { // avoid creating a new calendar object every time @@ -1001,23 +1121,53 @@ public class DrawPreview { // http://stackoverflow.com/questions/15981516/simpledateformat-gettimeinstance-ignores-24-hour-format // http://daniel-codes.blogspot.co.uk/2013/06/how-to-correctly-format-datetime.html // http://code.google.com/p/android/issues/detail?id=42104 + // update: now seems to be fixed // also possibly related https://code.google.com/p/android/issues/detail?id=181201 //int height = applicationInterface.drawTextWithBackground(canvas, p, current_time_string, Color.WHITE, Color.BLACK, location_x, location_y, MyApplicationInterface.Alignment.ALIGNMENT_TOP); if( text_bounds_time == null ) { if( MyDebug.LOG ) Log.d(TAG, "compute text_bounds_time"); text_bounds_time = new Rect(); - String bounds_time_string = "00:00:00"; + // better to not use a fixed string like "00:00:00" as don't want to make assumptions - e.g., in 12 hour format we'll have the appended am/pm to account for! + Calendar calendar = Calendar.getInstance(); + calendar.set(100, 0, 1, 10, 59, 59); + String bounds_time_string = dateFormatTimeInstance.format(calendar.getTime()); + if( MyDebug.LOG ) + Log.d(TAG, "bounds_time_string:" + bounds_time_string); p.getTextBounds(bounds_time_string, 0, bounds_time_string.length(), text_bounds_time); } + first_line_xshift += text_bounds_time.width() + gap_x; int height = applicationInterface.drawTextWithBackground(canvas, p, current_time_string, Color.WHITE, Color.BLACK, location_x, location_y, MyApplicationInterface.Alignment.ALIGNMENT_TOP, null, MyApplicationInterface.Shadow.SHADOW_OUTLINE, text_bounds_time); height += gap_y; - if( ui_rotation == 90 ) { - location_y -= height; + // don't update location_y yet, as we have time and cameraid shown on the same line + first_line_height = Math.max(first_line_height, height); + } + if( show_camera_id_pref && camera_controller != null ) { + if( camera_id_string == null || time_ms > last_camera_id_time + 10000 ) { + // cache string for performance + + camera_id_string = getContext().getResources().getString(R.string.camera_id) + ":" + preview.getCameraId(); // intentionally don't put a space + last_camera_id_time = time_ms; } - else { - location_y += height; + if( text_bounds_camera_id == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "compute text_bounds_camera_id"); + text_bounds_camera_id = new Rect(); + p.getTextBounds(camera_id_string, 0, camera_id_string.length(), text_bounds_camera_id); } + int xpos = align_right ? location_x - first_line_xshift : location_x + first_line_xshift; + int height = applicationInterface.drawTextWithBackground(canvas, p, camera_id_string, Color.WHITE, Color.BLACK, xpos, location_y, MyApplicationInterface.Alignment.ALIGNMENT_TOP, null, MyApplicationInterface.Shadow.SHADOW_OUTLINE, text_bounds_camera_id); + height += gap_y; + // don't update location_y yet, as we have time and cameraid shown on the same line + first_line_height = Math.max(first_line_height, height); + } + // update location_y for first line (time and camera id) + if( ui_rotation == 90 ) { + // upside-down portrait + location_y -= first_line_height; + } + else { + location_y += first_line_height; } if( camera_controller != null && show_free_memory_pref ) { @@ -1092,6 +1242,12 @@ public class DrawPreview { iso_exposure_string += " "; iso_exposure_string += preview.getFrameDurationString(frame_duration); } + /*if( camera_controller.captureResultHasAperture() ) { + float aperture = camera_controller.captureResultAperture(); + if( iso_exposure_string.length() > 0 ) + iso_exposure_string += " F"; + iso_exposure_string += decimal_format_1dp_force0.format(aperture); + }*/ is_scanning = false; if( camera_controller.captureResultIsAEScanning() ) { @@ -1154,12 +1310,15 @@ public class DrawPreview { canvas.drawRect(icon_dest, p); p.setAlpha(255); - if( applicationInterface.getLocation() != null ) { + Location location = applicationInterface.getLocation(locationInfo); + if( location != null ) { canvas.drawBitmap(location_bitmap, null, icon_dest, p); int location_radius = icon_size / 10; int indicator_x = location_x2 + icon_size - (int)(location_radius*1.5); int indicator_y = location_y + (int)(location_radius*1.5); - p.setColor(applicationInterface.getLocation().getAccuracy() < 25.01f ? Color.rgb(37, 155, 36) : Color.rgb(255, 235, 59)); // Green 500 or Yellow 500 + p.setColor(locationInfo.LocationWasCached() ? Color.rgb(127, 127, 127) : + location.getAccuracy() < 25.01f ? Color.rgb(37, 155, 36) : + Color.rgb(255, 235, 59)); // Green 500 or Yellow 500 canvas.drawCircle(indicator_x, indicator_y, location_radius, p); } else { @@ -1395,8 +1554,8 @@ public class DrawPreview { if( histogram != null ) { /*if( MyDebug.LOG ) Log.d(TAG, "histogram length: " + histogram.length);*/ - final int histogram_width = (int) (100 * scale + 0.5f); // convert dps to pixels - final int histogram_height = (int) (60 * scale + 0.5f); // convert dps to pixels + final int histogram_width = (int) (histogram_width_dp * scale + 0.5f); // convert dps to pixels + final int histogram_height = (int) (histogram_height_dp * scale + 0.5f); // convert dps to pixels // n.b., if changing the histogram_height, remember to update focus_seekbar and // focus_bracketing_target_seekbar margins in activity_main.xml int location_x2 = location_x - flash_padding; @@ -1479,10 +1638,10 @@ public class DrawPreview { * should be the maximum value of all histogram channels. */ private void drawHistogramChannel(Canvas canvas, int [] histogram_channel, int max) { - long debug_time = 0; + /*long debug_time = 0; if( MyDebug.LOG ) { debug_time = System.currentTimeMillis(); - } + }*/ /*if( MyDebug.LOG ) Log.d(TAG, "drawHistogramChannel, time before creating path: " + (System.currentTimeMillis() - debug_time));*/ @@ -1539,39 +1698,65 @@ public class DrawPreview { canvas.getHeight() / 2, p);*/ int gap_y = (int) (20 * scale + 0.5f); // convert dps to pixels int text_y = (int) (16 * scale + 0.5f); // convert dps to pixels + boolean avoid_ui = false; // fine tuning to adjust placement of text with respect to the GUI, depending on orientation if( ui_placement == MainUI.UIPlacement.UIPLACEMENT_TOP && ( ui_rotation == 0 || ui_rotation == 180 ) ) { - text_base_y = canvas.getHeight() - (int)(0.5*gap_y); + text_base_y = canvas.getHeight() - (int)(0.1*gap_y); + avoid_ui = true; } else if( ui_rotation == ( ui_placement == MainUI.UIPlacement.UIPLACEMENT_RIGHT ? 0 : 180 ) ) { - text_base_y = canvas.getHeight() - (int)(0.5*gap_y); + text_base_y = canvas.getHeight() - (int)(0.1*gap_y); + avoid_ui = true; } else if( ui_rotation == ( ui_placement == MainUI.UIPlacement.UIPLACEMENT_RIGHT ? 180 : 0 ) ) { text_base_y = canvas.getHeight() - (int)(2.5*gap_y); // leave room for GUI icons } else if( ui_rotation == 90 || ui_rotation == 270 ) { - //text_base_y = canvas.getHeight() + (int)(0.5*gap_y); - /*ImageButton view = (ImageButton)main_activity.findViewById(R.id.take_photo); - // align with "top" of the take_photo button, but remember to take the rotation into account! - view.getLocationOnScreen(gui_location); - int view_left = gui_location[0]; - preview.getView().getLocationOnScreen(gui_location); - int this_left = gui_location[0]; + // ui_rotation 90 is upside down portrait + // 270 is portrait + + if( last_take_photo_top_time == 0 || time_ms > last_take_photo_top_time + 1000 ) { + /*if( MyDebug.LOG ) + Log.d(TAG, "update cached take_photo_top");*/ + // don't call this too often, for UI performance (due to calling View.getLocationOnScreen()) + View view = main_activity.findViewById(R.id.take_photo); + // align with "top" of the take_photo button, but remember to take the rotation into account! + int view_left = getViewOnScreenX(view); + preview.getView().getLocationOnScreen(gui_location); + int this_left = gui_location[0]; + take_photo_top = view_left - this_left; + + last_take_photo_top_time = time_ms; + /*if( MyDebug.LOG ) { + Log.d(TAG, "view_left: " + view_left); + Log.d(TAG, "this_left: " + this_left); + Log.d(TAG, "take_photo_top: " + take_photo_top); + }*/ + } + // diff_x is the difference from the centre of the canvas to the position we want - int diff_x = view_left - ( this_left + canvas.getWidth()/2 ); - */ + int diff_x = take_photo_top - canvas.getWidth()/2; + /*if( MyDebug.LOG ) { Log.d(TAG, "view left: " + view_left); Log.d(TAG, "this left: " + this_left); Log.d(TAG, "canvas is " + canvas.getWidth() + " x " + canvas.getHeight()); + Log.d(TAG, "compare offset_x: " + (preview.getView().getRootView().getRight()/2 - diff_x)/scale); }*/ + // diff_x is the difference from the centre of the canvas to the position we want // assumes canvas is centered // avoids calling getLocationOnScreen for performance - int diff_x = preview.getView().getRootView().getRight()/2 - (int) (100 * scale + 0.5f); // convert dps to pixels + /*int offset_x = (int) (124 * scale + 0.5f); // convert dps to pixels + // offset_x should be enough such that on-screen level angle (this is the lowest display on-screen text) does not + // interfere with take photo icon when using at least a 16:9 preview aspect ratio + // should correspond to the logged "compare offset_x" above + int diff_x = preview.getView().getRootView().getRight()/2 - offset_x; + */ + int max_x = canvas.getWidth(); if( ui_rotation == 90 ) { - // so we don't interfere with the top bar info (datetime, free memory, ISO) + // so we don't interfere with the top bar info (datetime, free memory, ISO) when upside down max_x -= (int)(2.5*gap_y); } /*if( MyDebug.LOG ) { @@ -1581,12 +1766,28 @@ public class DrawPreview { Log.d(TAG, "max_x: " + max_x); }*/ if( canvas.getWidth()/2 + diff_x > max_x ) { - // in case goes off the size of the canvas, for "black bar" cases (when preview aspect ratio != screen aspect ratio) + // in case goes off the size of the canvas, for "black bar" cases (when preview aspect ratio < screen aspect ratio) diff_x = max_x - canvas.getWidth()/2; } text_base_y = canvas.getHeight()/2 + diff_x - (int)(0.5*gap_y); } + if( avoid_ui ) { + // avoid parts of the UI + View view = main_activity.findViewById(R.id.focus_seekbar); + if(view.getVisibility() == View.VISIBLE ) { + text_base_y -= view.getHeight(); + } + view = main_activity.findViewById(R.id.focus_bracketing_target_seekbar); + if(view.getVisibility() == View.VISIBLE ) { + text_base_y -= view.getHeight(); + } + /*view = main_activity.findViewById(R.id.sliders_container); + if(view.getVisibility() == View.VISIBLE ) { + text_base_y -= view.getHeight(); + }*/ + } + boolean draw_angle = has_level_angle && show_angle_pref; boolean draw_geo_direction = has_geo_direction && show_geo_direction_pref; if( draw_angle ) { @@ -1818,6 +2019,8 @@ public class DrawPreview { canvas.drawText(getContext().getResources().getString(R.string.failed_to_open_camera_1), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f, p); canvas.drawText(getContext().getResources().getString(R.string.failed_to_open_camera_2), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f + pixels_offset, p); canvas.drawText(getContext().getResources().getString(R.string.failed_to_open_camera_3), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f + 2 * pixels_offset, p); + // n.b., use applicationInterface.getCameraIdPref(), as preview.getCameraId() returns 0 if camera_controller==null + canvas.drawText(getContext().getResources().getString(R.string.camera_id) + ":" + applicationInterface.getCameraIdPref(), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f + 3 * pixels_offset, p); } } else { @@ -1832,23 +2035,67 @@ public class DrawPreview { int top_y = (int) (5 * scale + 0.5f); // convert dps to pixels View top_icon = main_activity.getMainUI().getTopIcon(); if( top_icon != null ) { - top_icon.getLocationOnScreen(gui_location); - int top_margin = gui_location[0] + top_icon.getWidth(); - preview.getView().getLocationOnScreen(gui_location); - int preview_left = gui_location[0]; - /*if( MyDebug.LOG ) - Log.d(TAG, "preview_left: " + preview_left);*/ - int shift = top_margin - preview_left; - if( shift > 0 ) { + if( last_top_icon_shift_time == 0 || time_ms > last_top_icon_shift_time + 1000 ) { + // avoid computing every time, due to cost of calling View.getLocationOnScreen() + /*if( MyDebug.LOG ) + Log.d(TAG, "update cached top_icon_shift");*/ + int top_margin = getViewOnScreenX(top_icon) + top_icon.getWidth(); + preview.getView().getLocationOnScreen(gui_location); + int preview_left = gui_location[0]; + this.top_icon_shift = top_margin - preview_left; + /*if( MyDebug.LOG ) { + Log.d(TAG, "top_icon.getRotation(): " + top_icon.getRotation()); + Log.d(TAG, "preview_left: " + preview_left); + Log.d(TAG, "top_margin: " + top_margin); + Log.d(TAG, "top_icon_shift: " + top_icon_shift); + }*/ + + last_top_icon_shift_time = time_ms; + } + + if( this.top_icon_shift > 0 ) { if( ui_rotation == 90 || ui_rotation == 270 ) { - top_y += shift; + // portrait + top_y += top_icon_shift; } else { - top_x += shift; + // landscape + top_x += top_icon_shift; } } } + { + /*int focus_seekbars_margin_left_dp = 85; + if( want_histogram ) + focus_seekbars_margin_left_dp += DrawPreview.histogram_height_dp;*/ + // 135 needed to make room for on-screen info lines in DrawPreview.onDrawInfoLines(), including the histogram + // but we also need to take the top_icon_shift into account, for widescreen aspect ratios and "icons along top" UI placement + int focus_seekbars_margin_left_dp = 135; + int new_focus_seekbars_margin_left = (int) (focus_seekbars_margin_left_dp * scale + 0.5f); // convert dps to pixels + if( top_icon_shift > 0 ) { + //noinspection SuspiciousNameCombination + new_focus_seekbars_margin_left += top_icon_shift; + } + + if( focus_seekbars_margin_left == -1 || new_focus_seekbars_margin_left != focus_seekbars_margin_left ) { + // we check whether focus_seekbars_margin_left has changed, in case there is a performance cost for setting layoutparams + this.focus_seekbars_margin_left = new_focus_seekbars_margin_left; + if( MyDebug.LOG ) + Log.d(TAG, "set focus_seekbars_margin_left to " + focus_seekbars_margin_left); + + View view = main_activity.findViewById(R.id.focus_seekbar); + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.setMargins(focus_seekbars_margin_left, 0, 0, 0); + view.setLayoutParams(layoutParams); + + view = main_activity.findViewById(R.id.focus_bracketing_target_seekbar); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.setMargins(focus_seekbars_margin_left, 0, 0, 0); + view.setLayoutParams(layoutParams); + } + } + int battery_x = top_x; int battery_y = top_y + (int) (5 * scale + 0.5f); int battery_width = (int) (5 * scale + 0.5f); // convert dps to pixels @@ -1913,7 +2160,10 @@ public class DrawPreview { } else actual_show_angle_line_pref = show_angle_line_pref; - if( camera_controller != null && !preview.isPreviewPaused() && has_level_angle && ( actual_show_angle_line_pref || show_pitch_lines_pref || show_geo_direction_lines_pref ) ) { + + boolean allow_angle_lines = camera_controller != null && !preview.isPreviewPaused(); + + if( allow_angle_lines && has_level_angle && ( actual_show_angle_line_pref || show_pitch_lines_pref || show_geo_direction_lines_pref ) ) { int ui_rotation = preview.getUIRotation(); double level_angle = preview.getLevelAngle(); boolean has_pitch_angle = preview.hasPitchAngle(); @@ -2109,6 +2359,49 @@ public class DrawPreview { canvas.restore(); } + + if( allow_angle_lines && auto_stabilise_pref && preview.hasLevelAngleStable() && !preview.isVideo() ) { + // although auto-level is supported for photos taken in video mode, there's the risk that it's misleading to display + // the guide when in video mode! + double level_angle = preview.getLevelAngle(); + double auto_stabilise_level_angle = level_angle; + //double auto_stabilise_level_angle = angle; + while( auto_stabilise_level_angle < -90 ) + auto_stabilise_level_angle += 180; + while( auto_stabilise_level_angle > 90 ) + auto_stabilise_level_angle -= 180; + double level_angle_rad_abs = Math.abs( Math.toRadians(auto_stabilise_level_angle) ); + + int w1 = canvas.getWidth(); + int h1 = canvas.getHeight(); + double w0 = (w1 * Math.cos(level_angle_rad_abs) + h1 * Math.sin(level_angle_rad_abs)); + double h0 = (w1 * Math.sin(level_angle_rad_abs) + h1 * Math.cos(level_angle_rad_abs)); + + if( ImageSaver.autoStabiliseCrop(auto_stabilise_crop, level_angle_rad_abs, w0, h0, w1, h1, canvas.getWidth(), canvas.getHeight()) ) { + int w2 = auto_stabilise_crop[0]; + int h2 = auto_stabilise_crop[1]; + int cx = canvas.getWidth()/2; + int cy = canvas.getHeight()/2; + + canvas.save(); + canvas.rotate((float)-level_angle, cx, cy); + + if( has_level_angle && Math.abs(level_angle) <= close_level_angle ) { // n.b., use level_angle, not angle or orig_level_angle + p.setColor(angle_highlight_color_pref); + } + else { + p.setColor(Color.WHITE); + } + p.setStyle(Paint.Style.STROKE); + p.setStrokeWidth(stroke_width); + + canvas.drawRect((canvas.getWidth() - w2)/2.0f, (canvas.getHeight() - h2)/2.0f, (canvas.getWidth() + w2)/2.0f, (canvas.getHeight() + h2)/2.0f, p); + + canvas.restore(); + + p.setStyle(Paint.Style.FILL); // reset + } + } } private void doThumbnailAnimation(Canvas canvas, long time_ms) { @@ -2291,7 +2584,7 @@ public class DrawPreview { preview.disableHistogram(); if( want_zebra_stripes ) - preview.enableZebraStripes(zebra_stripes_threshold); + preview.enableZebraStripes(zebra_stripes_threshold, zebra_stripes_color_foreground, zebra_stripes_color_background); else preview.disableZebraStripes(); @@ -2354,14 +2647,14 @@ public class DrawPreview { } setLastImageMatrix(canvas, last_thumbnail, ui_rotation, !show_last_image); if( !show_last_image ) - p.setAlpha(127); + p.setAlpha(ghost_image_alpha); canvas.drawBitmap(last_thumbnail, last_image_matrix, p); if( !show_last_image ) p.setAlpha(255); } else if( camera_controller != null && !front_screen_flash && ghost_selected_image_bitmap != null ) { setLastImageMatrix(canvas, ghost_selected_image_bitmap, ui_rotation, true); - p.setAlpha(127); + p.setAlpha(ghost_image_alpha); canvas.drawBitmap(ghost_selected_image_bitmap, last_image_matrix, p); p.setAlpha(255); } @@ -2505,14 +2798,14 @@ public class DrawPreview { } last_image_matrix.preRotate(this_ui_rotation, bitmap.getWidth()/2.0f, bitmap.getHeight()/2.0f); if( flip_front ) { - boolean is_front_facing = camera_controller != null && camera_controller.isFrontFacing(); + boolean is_front_facing = camera_controller != null && (camera_controller.getFacing() == CameraController.Facing.FACING_FRONT); if( is_front_facing && !sharedPreferences.getString(PreferenceKeys.FrontCameraMirrorKey, "preference_front_camera_mirror_no").equals("preference_front_camera_mirror_photo") ) { last_image_matrix.preScale(-1.0f, 1.0f, bitmap.getWidth()/2.0f, 0.0f); } } } - private void drawGyroSpot(Canvas canvas, float distance_x, float distance_y, float dir_x, float dir_y, int radius_dp, boolean outline) { + private void drawGyroSpot(Canvas canvas, float distance_x, float distance_y, @SuppressWarnings("unused") float dir_x, @SuppressWarnings("unused") float dir_y, int radius_dp, boolean outline) { if( outline ) { p.setStyle(Paint.Style.STROKE); p.setStrokeWidth(stroke_width); diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java index 4ffd856911ae8fc177568a515780e77114a1a969..4e43dd2ccfbcfccd63ec8cb8600d6431e4f95351 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java @@ -19,6 +19,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.media.AudioManager; import android.os.Build; +import android.os.Handler; import android.preference.PreferenceManager; import android.util.DisplayMetrics; import android.util.Log; @@ -33,6 +34,7 @@ import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Animation; import android.view.animation.ScaleAnimation; import android.widget.Button; +import android.widget.HorizontalScrollView; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -65,6 +67,7 @@ public class MainUI { private UIPlacement ui_placement = UIPlacement.UIPLACEMENT_RIGHT; private View top_icon = null; private boolean view_rotate_animation; + private final static int view_rotate_animation_duration = 100; // duration in ms of the icon rotation animation private boolean immersive_mode; private boolean show_gui_photo = true; // result of call to showGUI() - false means a "reduced" GUI is displayed, whilst taking photo or video @@ -91,6 +94,7 @@ public class MainUI { private final Map test_ui_buttons = new Hashtable<>(); public int test_saved_popup_width; public int test_saved_popup_height; + public volatile int test_navigation_gap; public MainUI(MainActivity main_activity) { if( MyDebug.LOG ) @@ -150,7 +154,7 @@ public class MainUI { rotate_by += 360.0f; // view.animate() modifies the view's rotation attribute, so it ends up equivalent to view.setRotation() // we use rotationBy() instead of rotation(), so we get the minimal rotation for clockwise vs anti-clockwise - view.animate().rotationBy(rotate_by).setDuration(100).setInterpolator(new AccelerateDecelerateInterpolator()).start(); + view.animate().rotationBy(rotate_by).setDuration(view_rotate_animation_duration).setInterpolator(new AccelerateDecelerateInterpolator()).start(); } public void layoutUI() { @@ -238,9 +242,13 @@ public class MainUI { iconpanel_right_of = RelativeLayout.ABOVE; iconpanel_above = RelativeLayout.LEFT_OF; iconpanel_below = RelativeLayout.RIGHT_OF; + //noinspection SuspiciousNameCombination iconpanel_align_parent_left = RelativeLayout.ALIGN_PARENT_BOTTOM; + //noinspection SuspiciousNameCombination iconpanel_align_parent_right = RelativeLayout.ALIGN_PARENT_TOP; + //noinspection SuspiciousNameCombination iconpanel_align_parent_top = RelativeLayout.ALIGN_PARENT_LEFT; + //noinspection SuspiciousNameCombination iconpanel_align_parent_bottom = RelativeLayout.ALIGN_PARENT_RIGHT; } @@ -249,6 +257,25 @@ public class MainUI { display.getSize(display_size); final int display_height = Math.min(display_size.x, display_size.y); + /*int navigation_gap = 0; + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ) { + final int display_width = Math.max(display_size.x, display_size.y); + Point real_display_size = new Point(); + display.getRealSize(real_display_size); + final int real_display_width = Math.max(real_display_size.x, real_display_size.y); + navigation_gap = real_display_width - display_width; + if( MyDebug.LOG ) { + Log.d(TAG, "display_width: " + display_width); + Log.d(TAG, "real_display_width: " + real_display_width); + Log.d(TAG, "navigation_gap: " + navigation_gap); + } + }*/ + int navigation_gap = main_activity.getNavigationGap(); + test_navigation_gap = navigation_gap; + if( MyDebug.LOG ) { + Log.d(TAG, "navigation_gap: " + navigation_gap); + } + if( !popup_container_only ) { // reset: @@ -282,6 +309,7 @@ public class MainUI { layoutParams.addRule(below, 0); layoutParams.addRule(left_of, 0); layoutParams.addRule(right_of, 0); + layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); } @@ -415,6 +443,7 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -422,6 +451,12 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.switch_multi_camera); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -429,6 +464,7 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -436,6 +472,7 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -443,6 +480,7 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -450,6 +488,7 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -459,6 +498,7 @@ public class MainUI { layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); layoutParams.addRule(align_parent_top, 0); layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); view.setRotation(180.0f); // should always match the zoom_seekbar, so that zoom in and out are in the same directions @@ -475,12 +515,14 @@ public class MainUI { layoutParams.addRule(align_parent_right, 0); layoutParams.addRule(align_parent_top, 0); layoutParams.addRule(align_parent_bottom, 0); + layoutParams.setMargins(0, 0, 0, 0); } else { layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); layoutParams.addRule(align_parent_top, 0); layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + layoutParams.setMargins(0, 0, navigation_gap, 0); // need to clear the others, in case we turn zoom controls on/off layoutParams.addRule(align_left, 0); layoutParams.addRule(align_right, 0); @@ -515,9 +557,11 @@ public class MainUI { // set seekbar info int width_dp; if( ui_rotation == 0 || ui_rotation == 180 ) { + // landscape width_dp = 350; } else { + // portrait width_dp = 250; // prevent being too large on smaller devices (e.g., Galaxy Nexus or smaller) int max_width_dp = getMaxHeightDp(true); @@ -535,16 +579,72 @@ public class MainUI { setViewRotation(view, ui_rotation); view.setTranslationX(0.0f); view.setTranslationY(0.0f); + if( ui_rotation == 90 || ui_rotation == 270 ) { + // portrait view.setTranslationX(2*height_pixels); } else if( ui_rotation == 0 ) { + // landscape view.setTranslationY(height_pixels); } else { + // upside-down landscape view.setTranslationY(-1*height_pixels); } + /* + // align sliders_container + RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)view.getLayoutParams(); + if( ui_rotation == 90 || ui_rotation == 270 ) { + // portrait + view.setTranslationX(2*height_pixels); + lp.addRule(left_of, 0); + lp.addRule(right_of, 0); + lp.addRule(above, 0); + lp.addRule(below, 0); + lp.addRule(align_parent_top, 0); + lp.addRule(align_parent_bottom, 0); + } + else if( ui_rotation == (ui_placement == UIPlacement.UIPLACEMENT_LEFT ? 180 : 0) ) { + // landscape (or upside-down landscape if ui-left) + view.setTranslationY(0); + lp.addRule(left_of, R.id.zoom_seekbar); + lp.addRule(right_of, 0); + + if( main_activity.showManualFocusSeekbar(true) ) { + lp.addRule(above, R.id.focus_bracketing_target_seekbar); + lp.addRule(below, 0); + lp.addRule(align_parent_top, 0); + lp.addRule(align_parent_bottom, 0); + } + else if( main_activity.showManualFocusSeekbar(false) ) { + lp.addRule(above, R.id.focus_seekbar); + lp.addRule(below, 0); + lp.addRule(align_parent_top, 0); + lp.addRule(align_parent_bottom, 0); + } + else { + lp.addRule(above, 0); + lp.addRule(below, 0); + lp.addRule(align_parent_top, 0); + lp.addRule(align_parent_bottom, RelativeLayout.TRUE); + } + } + else { + // upside-down landscape (or landscape if ui-left) + if( ui_rotation == 0 ) + view.setTranslationY(height_pixels); + else + view.setTranslationY(-1*height_pixels); + lp.addRule(left_of, 0); + lp.addRule(right_of, 0); + lp.addRule(above, 0); + lp.addRule(below, 0); + lp.addRule(align_parent_bottom, 0); + } + view.setLayoutParams(lp);*/ + view = main_activity.findViewById(R.id.exposure_seekbar); RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)view.getLayoutParams(); lp.width = width_pixels; @@ -681,8 +781,10 @@ public class MainUI { else if( ui_placement == UIPlacement.UIPLACEMENT_TOP ) { view.setPivotX(0.0f); view.setPivotY(0.0f); - if( ui_rotation == 90 ) + if( ui_rotation == 90 ) { + //noinspection SuspiciousNameCombination view.setTranslationX(popup_height); + } else if( ui_rotation == 270 ) { view.setTranslationY(display_height); } @@ -691,8 +793,10 @@ public class MainUI { view.setPivotX(popup_width); view.setPivotY(ui_placement == UIPlacement.UIPLACEMENT_RIGHT ? 0.0f : popup_height); if( ui_placement == UIPlacement.UIPLACEMENT_RIGHT ) { - if( ui_rotation == 90 ) + if( ui_rotation == 90 ) { + //noinspection SuspiciousNameCombination view.setTranslationY( popup_width ); + } else if( ui_rotation == 270 ) view.setTranslationX( - popup_height ); } @@ -759,11 +863,19 @@ public class MainUI { ImageButton view = main_activity.findViewById(R.id.switch_camera); int content_description; int cameraId = main_activity.getNextCameraId(); - if( main_activity.getPreview().getCameraControllerManager().isFrontFacing( cameraId ) ) { - content_description = R.string.switch_to_front_camera; - } - else { - content_description = R.string.switch_to_back_camera; + switch( main_activity.getPreview().getCameraControllerManager().getFacing( cameraId ) ) { + case FACING_FRONT: + content_description = R.string.switch_to_front_camera; + break; + case FACING_BACK: + content_description = R.string.switch_to_back_camera; + break; + case FACING_EXTERNAL: + content_description = R.string.switch_to_external_camera; + break; + default: + content_description = R.string.switch_to_unknown_camera; + break; } if( MyDebug.LOG ) Log.d(TAG, "content_description: " + main_activity.getResources().getString(content_description)); @@ -832,6 +944,29 @@ public class MainUI { view_rotate_animation = true; layoutUI(); view_rotate_animation = false; + + // Call DrawPreview.updateSettings() so that we reset calculations that depend on + // getLocationOnScreen() - since the result is affected by a View's rotation, we need + // to recompute - this also means we need to delay slightly until after the rotation + // animation is complete. + // To reproduce issues, rotate from upside-down-landscape to portrait, and observe + // the info-text placement (when using icons-along-top), or with on-screen angle + // displayed when in 16:9 preview. + // Potentially we could use Animation.setAnimationListener(), but we set a separate + // animation for every icon. + // Note, this seems to be unneeded due to the fix in DrawPreview for + // "getRotation() == 180.0f", but good to clear the cached values (e.g., in case we + // compute them during when the icons are being rotated). + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "onOrientationChanged->postDelayed()"); + + main_activity.getApplicationInterface().getDrawPreview().updateSettings(); + } + }, view_rotate_animation_duration+20); } } } @@ -884,6 +1019,8 @@ public class MainUI { public boolean showCycleFlashIcon() { if( !main_activity.getPreview().supportsFlash() ) return false; + if( main_activity.getPreview().isVideo() ) + return false; // no point showing flash icon in video mode, as we only allow flash auto and flash torch, and we don't support torch on the on-screen cycle flash icon SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); return sharedPreferences.getBoolean(PreferenceKeys.ShowCycleFlashPreferenceKey, false); } @@ -909,6 +1046,7 @@ public class MainUI { Log.d(TAG, "setImmersiveMode: set visibility: " + visibility); // n.b., don't hide share and trash buttons, as they require immediate user input for us to continue View switchCameraButton = main_activity.findViewById(R.id.switch_camera); + View switchMultiCameraButton = main_activity.findViewById(R.id.switch_multi_camera); View switchVideoButton = main_activity.findViewById(R.id.switch_video); View exposureButton = main_activity.findViewById(R.id.exposure); View exposureLockButton = main_activity.findViewById(R.id.exposure_lock); @@ -928,6 +1066,8 @@ public class MainUI { View zoomSeekBar = main_activity.findViewById(R.id.zoom_seekbar); if( main_activity.getPreview().getCameraControllerManager().getNumberOfCameras() > 1 ) switchCameraButton.setVisibility(visibility); + if( main_activity.showSwitchMultiCamIcon() ) + switchMultiCameraButton.setVisibility(visibility); switchVideoButton.setVisibility(visibility); if( main_activity.supportsExposureButton() ) exposureButton.setVisibility(visibility); @@ -1024,6 +1164,7 @@ public class MainUI { final int visibility = is_panorama_recording ? View.GONE : (show_gui_photo && show_gui_video) ? View.VISIBLE : View.GONE; // for UI that is hidden while taking photo or video final int visibility_video = is_panorama_recording ? View.GONE : show_gui_photo ? View.VISIBLE : View.GONE; // for UI that is only hidden while taking photo View switchCameraButton = main_activity.findViewById(R.id.switch_camera); + View switchMultiCameraButton = main_activity.findViewById(R.id.switch_multi_camera); View switchVideoButton = main_activity.findViewById(R.id.switch_video); View exposureButton = main_activity.findViewById(R.id.exposure); View exposureLockButton = main_activity.findViewById(R.id.exposure_lock); @@ -1039,6 +1180,8 @@ public class MainUI { View popupButton = main_activity.findViewById(R.id.popup); if( main_activity.getPreview().getCameraControllerManager().getNumberOfCameras() > 1 ) switchCameraButton.setVisibility(visibility); + if( main_activity.showSwitchMultiCamIcon() ) + switchMultiCameraButton.setVisibility(visibility); switchVideoButton.setVisibility(visibility); if( main_activity.supportsExposureButton() ) exposureButton.setVisibility(visibility_video); // still allow exposure when recording video @@ -1144,7 +1287,9 @@ public class MainUI { } public void updateCycleFlashIcon() { - String flash_value = main_activity.getApplicationInterface().getFlashPref(); + // n.b., read from preview rather than saved application preference - so the icon updates correctly when in flash + // auto mode, but user switches to manual ISO where flash auto isn't supported + String flash_value = main_activity.getPreview().getCurrentFlashValue(); if( flash_value != null ) { ImageButton view = main_activity.findViewById(R.id.cycle_flash); switch( flash_value ) { @@ -1166,8 +1311,17 @@ public class MainUI { case "flash_red_eye": view.setImageResource(R.drawable.baseline_remove_red_eye_white_48); break; + default: + // just in case?? + Log.e(TAG, "unknown flash value " + flash_value); + view.setImageResource(R.drawable.flash_off); + break; } } + else { + ImageButton view = main_activity.findViewById(R.id.cycle_flash); + view.setImageResource(R.drawable.flash_off); + } } public void updateFaceDetectionIcon() { @@ -1220,7 +1374,7 @@ public class MainUI { closePopup(); mSelectingExposureUIElement = false; if( isExposureUIOpen() ) { - clearSeekBar(); + closeExposureUI(); } else if( main_activity.getPreview().getCameraController() != null ) { setupExposureUI(); @@ -1325,7 +1479,6 @@ public class MainUI { if (mExposureLine == 0) { iso_buttons_container.setBackgroundColor(highlightColor); //iso_buttons_container.setAlpha(0.5f); - return; } else if (mExposureLine == 1) { iso_seekbar.setBackgroundColor(highlightColor); //iso_seekbar.setAlpha(0.5f); @@ -1597,12 +1750,16 @@ public class MainUI { private int iso_button_manual_index = -1; private final static String manual_iso_value = "m"; + /** Opens the exposure UI if not already open, and sets up or updates the UI. + */ public void setupExposureUI() { if( MyDebug.LOG ) Log.d(TAG, "setupExposureUI"); test_ui_buttons.clear(); final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); final Preview preview = main_activity.getPreview(); + ImageButton view = main_activity.findViewById(R.id.exposure); + view.setImageResource(R.drawable.ic_exposure_red_48dp); View sliders_container = main_activity.findViewById(R.id.sliders_container); sliders_container.setVisibility(View.VISIBLE); ViewGroup iso_buttons_container = main_activity.findViewById(R.id.iso_buttons); @@ -1667,7 +1824,8 @@ public class MainUI { // also reset exposure time when changing from manual to auto from the popup menu: editor.putLong(PreferenceKeys.ExposureTimePreferenceKey, CameraController.EXPOSURE_TIME_DEFAULT); editor.apply(); - main_activity.updateForSettings("ISO: " + toast_option); + preview.showToast("ISO: " + toast_option, 0, true); // supply offset_y_dp to be consistent with preview.setExposure(), preview.setISO() + main_activity.updateForSettings(""); // already showed the toast, so block from showing again } else if( old_iso.equals(CameraController.ISO_DEFAULT) ) { if( MyDebug.LOG ) @@ -1704,7 +1862,8 @@ public class MainUI { } editor.apply(); - main_activity.updateForSettings("ISO: " + toast_option); + preview.showToast("ISO: " + toast_option, 0, true); // supply offset_y_dp to be consistent with preview.setExposure(), preview.setISO() + main_activity.updateForSettings(""); // already showed the toast, so block from showing again } else { if( MyDebug.LOG ) @@ -1791,6 +1950,8 @@ public class MainUI { else { manual_white_balance_seek_bar.setVisibility(View.GONE); } + + //layoutUI(); // needed to update alignment of exposure UI } /** If the exposure panel is open, updates the selected ISO button to match the current ISO value, @@ -1862,7 +2023,12 @@ public class MainUI { } } - public void clearSeekBar() { + /** Closes the exposure UI. + */ + public void closeExposureUI() { + ImageButton image_button = main_activity.findViewById(R.id.exposure); + image_button.setImageResource(R.drawable.ic_exposure_white_48dp); + clearRemoteControlForExposureUI(); // must be called before we actually close the exposure panel View view = main_activity.findViewById(R.id.sliders_container); view.setVisibility(View.GONE); @@ -1959,7 +2125,6 @@ public class MainUI { /** * Higlights the next LinearLayout view - * @param highlight */ private void highlightPopupLine(boolean highlight, boolean goUp) { if( MyDebug.LOG ) { @@ -1983,6 +2148,11 @@ public class MainUI { // Ensure we stay within our bounds: mPopupLine = (mPopupLine + count ) % count; View v = inside.getChildAt(mPopupLine); + if( MyDebug.LOG ) + Log.d(TAG, "line: " + mPopupLine + " view: " + v); + // to test example with HorizontalScrollView, see popup menu on Nokia 8 with Camera2 API, the flash icons row uses a HorizontalScrollView + if( v instanceof HorizontalScrollView && ((HorizontalScrollView) v).getChildCount() > 0 ) + v = ((HorizontalScrollView) v).getChildAt(0); if (v.isShown() && v instanceof LinearLayout ) { if (highlight) { v.setBackgroundColor(highlightColor); @@ -1995,6 +2165,8 @@ public class MainUI { v.setAlpha(1f); } foundLine = true; + if( MyDebug.LOG ) + Log.d(TAG, "found at line: " + foundLine); } else { mPopupLine += goUp ? -1 : 1; } @@ -2007,8 +2179,6 @@ public class MainUI { * Highlights an icon on a horizontal line, such as flash mode, * focus mode, etc. Checks that the popup is open in case it is * wrongly called, so that it doesn't crash the app. - * @param highlight - * @param goLeft */ private void highlightPopupIcon(boolean highlight, boolean goLeft) { if( MyDebug.LOG ) { @@ -2028,6 +2198,8 @@ public class MainUI { // (careful, modulo in Java will allow negative numbers, hence the line below: mPopupIcon= (mPopupIcon + count ) % count; View v = mHighlightedLine.getChildAt(mPopupIcon); + if( MyDebug.LOG ) + Log.d(TAG, "row: " + mPopupIcon + " view: " + v); if (v instanceof ImageButton || v instanceof Button ) { if (highlight) { v.setBackgroundColor(highlightColor); @@ -2037,6 +2209,8 @@ public class MainUI { } else { v.setBackgroundColor(Color.TRANSPARENT); } + if( MyDebug.LOG ) + Log.d(TAG, "found icon at row: " + mPopupIcon); foundIcon = true; } else { mPopupIcon+= goLeft ? -1 : 1; @@ -2117,7 +2291,7 @@ public class MainUI { if( MyDebug.LOG ) Log.d(TAG, "open popup"); - clearSeekBar(); + closeExposureUI(); main_activity.getPreview().cancelTimer(); // best to cancel any timer, in case we take a photo while settings window is open, or when changing settings main_activity.stopAudioListeners(); diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/ManualSeekbars.java b/app/src/main/java/net/sourceforge/opencamera/ui/ManualSeekbars.java index 7af0f53ee6c13436c6797d41c5917833a9e20d26..148a296d361bd67413a128bc8ef121f3da0609ff 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/ManualSeekbars.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/ManualSeekbars.java @@ -218,7 +218,30 @@ public class ManualSeekbars { } // 20 to 60, per 5s - for(int i=20;i<=60;i+=5) { + for(int i=20;i<60;i+=5) { + long exposure = 1000000000L*i; + if( exposure > min_exposure_time && exposure < max_exposure_time ) + seekbar_values.add(exposure); + } + + // n.b., very long exposure times are not widely supported, but requested at https://sourceforge.net/p/opencamera/code/merge-requests/49/ + + // 60 to 180, per 15s + for(int i=60;i<180;i+=15) { + long exposure = 1000000000L*i; + if( exposure > min_exposure_time && exposure < max_exposure_time ) + seekbar_values.add(exposure); + } + + // 180 to 600, per 60s + for(int i=180;i<600;i+=60) { + long exposure = 1000000000L*i; + if( exposure > min_exposure_time && exposure < max_exposure_time ) + seekbar_values.add(exposure); + } + + // 600 to 1200, per 120s + for(int i=600;i<=1200;i+=120) { long exposure = 1000000000L*i; if( exposure > min_exposure_time && exposure < max_exposure_time ) seekbar_values.add(exposure); diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java b/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java index f6374c36c72ff6caf7accbd00b30bc87404d6b96..4c3da1b7766e6e8ac836ddb2f0754dff2333d333 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java @@ -8,6 +8,7 @@ import foundation.e.camera.R; import net.sourceforge.opencamera.cameracontroller.CameraController; import net.sourceforge.opencamera.preview.Preview; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -69,6 +70,9 @@ public class PopupView extends LinearLayout { private int repeat_mode_index = -1; private int grid_index = -1; + @SuppressWarnings("FieldCanBeLocal") + private final DecimalFormat decimal_format_1dp_force0 = new DecimalFormat("0.0"); + public PopupView(Context context) { super(context); if( MyDebug.LOG ) @@ -358,8 +362,8 @@ public class PopupView extends LinearLayout { final List picture_size_strings = new ArrayList<>(); for(int i=0;i apertures = new ArrayList<>(); + final List apertures_strings = new ArrayList<>(); + float current_aperture = main_activity.getApplicationInterface().getAperturePref(); + String prefix = "F/"; + + boolean found_default = false; + String current_aperture_s = ""; + for(float aperture : preview.getSupportedApertures()) { + apertures.add(aperture); + String aperture_string = prefix + decimal_format_1dp_force0.format(aperture); + apertures_strings.add(aperture_string); + if( current_aperture == aperture ) { + found_default = true; + current_aperture_s = aperture_string; + } + } + + if( !found_default ) { + // read from Camera API + if( preview.getCameraController() != null && preview.getCameraController().captureResultHasAperture() ) { + current_aperture = preview.getCameraController().captureResultAperture(); + current_aperture_s = prefix + decimal_format_1dp_force0.format(current_aperture); + } + } + + addButtonOptionsToPopup(apertures_strings, -1, -1, "", current_aperture_s, 0, "TEST_APERTURE", new ButtonOptionsPopupListener() { + @Override + public void onClick(String option) { + if( MyDebug.LOG ) + Log.d(TAG, "clicked aperture: " + option); + int index = apertures_strings.indexOf(option); + if( index != -1 ) { + float new_aperture = apertures.get(index); + if( MyDebug.LOG ) + Log.d(TAG, "new_aperture: " + new_aperture); + preview.showToast(null, getResources().getString(R.string.aperture) + ": " + option); + main_activity.getApplicationInterface().setAperture(new_aperture); + if( preview.getCameraController() != null ) { + preview.getCameraController().setAperture(new_aperture); + } + } + else { + Log.e(TAG, "unknown aperture: " + option); + } + main_activity.getMainUI().destroyPopup(); // need to recreate popup for new selection + } + }); + } + if( !preview.isVideo() && photo_mode == MyApplicationInterface.PhotoMode.FastBurst ) { if( MyDebug.LOG ) Log.d(TAG, "add fast burst options"); @@ -788,7 +847,7 @@ public class PopupView extends LinearLayout { final String [] timer_values = getResources().getStringArray(R.array.preference_timer_values); String [] timer_entries = getResources().getStringArray(R.array.preference_timer_entries); - String timer_value = sharedPreferences.getString(PreferenceKeys.getTimerPreferenceKey(), "0"); + String timer_value = sharedPreferences.getString(PreferenceKeys.TimerPreferenceKey, "0"); timer_index = Arrays.asList(timer_values).indexOf(timer_value); if( timer_index == -1 ) { if( MyDebug.LOG ) @@ -803,7 +862,7 @@ public class PopupView extends LinearLayout { String new_timer_value = timer_values[timer_index]; SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(PreferenceKeys.getTimerPreferenceKey(), new_timer_value); + editor.putString(PreferenceKeys.TimerPreferenceKey, new_timer_value); editor.apply(); } @Override @@ -834,7 +893,7 @@ public class PopupView extends LinearLayout { final String [] repeat_mode_values = getResources().getStringArray(R.array.preference_burst_mode_values); String [] repeat_mode_entries = getResources().getStringArray(R.array.preference_burst_mode_entries); - String repeat_mode_value = sharedPreferences.getString(PreferenceKeys.getRepeatModePreferenceKey(), "1"); + String repeat_mode_value = sharedPreferences.getString(PreferenceKeys.RepeatModePreferenceKey, "1"); repeat_mode_index = Arrays.asList(repeat_mode_values).indexOf(repeat_mode_value); if( repeat_mode_index == -1 ) { if( MyDebug.LOG ) @@ -850,7 +909,7 @@ public class PopupView extends LinearLayout { String new_repeat_mode_value = repeat_mode_values[repeat_mode_index]; SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(PreferenceKeys.getRepeatModePreferenceKey(), new_repeat_mode_value); + editor.putString(PreferenceKeys.RepeatModePreferenceKey, new_repeat_mode_value); editor.apply(); } @Override @@ -1125,6 +1184,11 @@ public class PopupView extends LinearLayout { editor.apply(); } // otherwise default to the saved value + + if( !main_activity.getMainUI().isExposureUIOpen() ) { + // also open the exposure UI, to show the + main_activity.getMainUI().toggleExposureUI(); + } } } } @@ -1409,10 +1473,11 @@ public class PopupView extends LinearLayout { text_view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, title_text_size_dip); text_view.setTypeface(null, Typeface.BOLD); //text_view.setBackgroundColor(Color.GRAY); // debug + text_view.setBackgroundColor(Color.argb(255, 33, 33, 33)); // Grey 900 this.addView(text_view); } - private abstract class RadioOptionsListener { + private abstract static class RadioOptionsListener { /** Called when a radio option is selected. * @param selected_value The entry in the supplied supported_options_values list (received * by addRadioOptionsToPopup) that corresponds to the selected radio @@ -1600,7 +1665,7 @@ public class PopupView extends LinearLayout { Log.d(TAG, "addRadioOptionsToGroup time total: " + (System.nanoTime() - debug_time)); } - private abstract class ArrayOptionsPopupListener { + private abstract static class ArrayOptionsPopupListener { protected abstract int onClickPrev(); protected abstract int onClickNext(); } @@ -1642,10 +1707,17 @@ public class PopupView extends LinearLayout { final TextView text_view = new TextView(this.getContext()); setArrayOptionsText(supported_options, title, text_view, title_in_options, title_in_options_first_only, current_index); + //text_view.setBackgroundColor(Color.GRAY); // debug text_view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, standard_text_size_dip); text_view.setTextColor(Color.WHITE); text_view.setGravity(Gravity.CENTER); + text_view.setSingleLine(true); // if text too long for the button, we'd rather not have wordwrap, even if it means cutting some text off LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f); + // Yuck! We want the arrow_button_w to be fairly large so that users can touch the arrow buttons easily, but if + // the text is too much for the button size, we'd rather it extend into the arrow buttons (which the user won't see + // anyway, since the button backgrounds are transparent). + // Needed for OnePlus 3T and Nokia 8, for camera resolution + params.setMargins(-arrow_button_w/2, 0, -arrow_button_w/2, 0); text_view.setLayoutParams(params); final float scale = getResources().getDisplayMetrics().density; diff --git a/app/src/main/res/drawable-hdpi/baseline_add_a_photo_white_48.png b/app/src/main/res/drawable-hdpi/baseline_add_a_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..be070f73b6842e4b4d35fe3ef341291d9642601c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/baseline_add_a_photo_white_48.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_exposure_red_48dp.png b/app/src/main/res/drawable-hdpi/ic_exposure_red_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..25207a9d29883b56f02470372354052196b7b78e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_exposure_red_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/baseline_add_a_photo_white_48.png b/app/src/main/res/drawable-mdpi/baseline_add_a_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cb5d4d4fa669143cb3b249f2440a2a686b0435 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/baseline_add_a_photo_white_48.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_exposure_red_48dp.png b/app/src/main/res/drawable-mdpi/ic_exposure_red_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2c800032750973bb67282a552f84a000c916fa17 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_exposure_red_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/take_video.png b/app/src/main/res/drawable-mdpi/take_video.png index baa7d15e4f21d208e1f81c1258788e56914a6473..dbd02fb220262e6529a608242040b222b4fb64c9 100644 Binary files a/app/src/main/res/drawable-mdpi/take_video.png and b/app/src/main/res/drawable-mdpi/take_video.png differ diff --git a/app/src/main/res/drawable-mdpi/take_video_pressed.png b/app/src/main/res/drawable-mdpi/take_video_pressed.png index 9ec59ff250afe7882065cdc4a7e0b4259e5604ed..b53a69345aeffeca7dd0fa8e21b9bbcbdcfd623b 100644 Binary files a/app/src/main/res/drawable-mdpi/take_video_pressed.png and b/app/src/main/res/drawable-mdpi/take_video_pressed.png differ diff --git a/app/src/main/res/drawable-mdpi/take_video_recording.png b/app/src/main/res/drawable-mdpi/take_video_recording.png index 74d16a0214f8e2814838aa7d943b2c089300621d..02efddf195ce9b7fb15037258598f63a4e3c5346 100644 Binary files a/app/src/main/res/drawable-mdpi/take_video_recording.png and b/app/src/main/res/drawable-mdpi/take_video_recording.png differ diff --git a/app/src/main/res/drawable-xhdpi/baseline_add_a_photo_white_48.png b/app/src/main/res/drawable-xhdpi/baseline_add_a_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..e481f66628879542551faa6ad26ec886205004dc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/baseline_add_a_photo_white_48.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_exposure_red_48dp.png b/app/src/main/res/drawable-xhdpi/ic_exposure_red_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0868efb210be3f2975132a23cb69181e387f6644 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_exposure_red_48dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/take_video.png b/app/src/main/res/drawable-xhdpi/take_video.png new file mode 100644 index 0000000000000000000000000000000000000000..baa7d15e4f21d208e1f81c1258788e56914a6473 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/take_video.png differ diff --git a/app/src/main/res/drawable-xhdpi/take_video_pressed.png b/app/src/main/res/drawable-xhdpi/take_video_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..9ec59ff250afe7882065cdc4a7e0b4259e5604ed Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/take_video_pressed.png differ diff --git a/app/src/main/res/drawable-xhdpi/take_video_recording.png b/app/src/main/res/drawable-xhdpi/take_video_recording.png new file mode 100644 index 0000000000000000000000000000000000000000..74d16a0214f8e2814838aa7d943b2c089300621d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/take_video_recording.png differ diff --git a/app/src/main/res/drawable-xxhdpi/baseline_add_a_photo_white_48.png b/app/src/main/res/drawable-xxhdpi/baseline_add_a_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..3cc5b100d22a2c93c871820a1bb480831bec2213 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/baseline_add_a_photo_white_48.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_exposure_red_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_exposure_red_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..47818b9378c7334fe22a36859b39bdba3eb00a00 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_exposure_red_48dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/baseline_add_a_photo_white_48.png b/app/src/main/res/drawable-xxxhdpi/baseline_add_a_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe202d967424bd09636f789d007451896d8f941 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/baseline_add_a_photo_white_48.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_exposure_red_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_exposure_red_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1d841672efbe3604fea36d63d0f4880c3e8ce521 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_exposure_red_48dp.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b602e98e8f5ee89cb6aef522dace8c25e23f16cd..ba69625243bd1e431fcca23791552452c68b47b8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -39,6 +39,29 @@ android:backgroundTintMode="src_in" /> + + - + - + @@ -516,8 +537,8 @@ @@ -536,8 +557,8 @@ @@ -567,8 +588,8 @@ @@ -609,6 +630,7 @@ android:layout_height="match_parent" android:background="@android:color/black" android:visibility="gone" + android:importantForAccessibility="no" /> diff --git a/app/src/main/res/layout/arrayseekbarpreference.xml b/app/src/main/res/layout/arrayseekbarpreference.xml new file mode 100644 index 0000000000000000000000000000000000000000..97c97f4c9a537e93c25d265f218d7deaf518ebb1 --- /dev/null +++ b/app/src/main/res/layout/arrayseekbarpreference.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/values-az/arrays.xml b/app/src/main/res/values-az/arrays.xml index 91bb044fd9a6600004efb13c60d9d2ce85ec3177..f6091336a8b6a2470cb6debd524f6ba1dfaeee1b 100644 --- a/app/src/main/res/values-az/arrays.xml +++ b/app/src/main/res/values-az/arrays.xml @@ -94,18 +94,6 @@ 0 180 - - Bağla - Hərəkət düyməsini gizlə - GUI gizlə - Hamısını gizlə - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Yox 3x3 diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index d5b3034d82ed1d962c8ee57c558d29252947e661..ab476705bdaf68cca967b62734a474c9632672cb 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -91,7 +91,7 @@ Çəkilişdən sonra fasilə Şəkil çəkildikdən sonra paylaşma və ya silinmə imkanları yaradan fasilə Çəkiliş səsi - Şəkil çəkilən zaman eşidilən səs (bağlanması üçün Android 4.2+ tələb olunur) + Şəkil çəkilən zaman eşidilən səs Səs səviyyəsi düyməsi Saxlanma yeri Şəkil və videonun saxlanması üçün qovluq adı @@ -182,7 +182,7 @@ Dəstək üçün İanə Əgər bu proqram xoşunuza gəlirsə, lütfən ianə vermək haqqında düşünün. Siz bunu Proqram üçün ianə - toxunaraq almaqla edə bilərsiniz, bu funksiya proqrama ianə vermək üçün səhifəni açacaq. Təşəkkürlər! Camera2 API istifadəsi - Android 5 Camera2 API işə salınması - əlavə funksiyalar təqdim edir, amma hazırda bu ekperimentaldır (yenidənbaşlamalara səbəb ola bilər) + Camera2 API işə salınması - əlavə funksiyalar təqdim edir, amma hazırda bu ekperimentaldır (yenidənbaşlamalara səbəb ola bilər) Haqqında Proqram məlumatları Parametrləri sıfırla @@ -276,6 +276,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Sonsuz Fasiləsiz @@ -319,4 +326,9 @@ Kamera məlumatları Xidmət şərtləri + Bağla + Hərəkət düyməsini gizlə + GUI gizlə + Hamısını gizlə + diff --git a/app/src/main/res/values-be/arrays.xml b/app/src/main/res/values-be/arrays.xml index 0f861c0ef508ac0903f48274ad3fdcce2c2fb0c4..5bb6de23ae9bfe6bdb11a6b9109d6ef501f8d6c4 100644 --- a/app/src/main/res/values-be/arrays.xml +++ b/app/src/main/res/values-be/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Выключана - Хаваць віртуальныя кнопкі навігацыі на экране - Хаваць GUI - Хаваць усе - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Чырвоны Зялёны diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 9893611cc524ecc2f49ec515e0a93fadbf4a6bd9..1cba9d7af4ddcef5b3b062b0b237b2bab367ed42 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -103,12 +103,12 @@ Паўза пасля фатаграфавання Паўза пасля фатаграфавання, падчас якой магчыма падзяліцца або выдаліць фота Гук засаўкі - Прайграванне гуку пры фатаграфаванні (патрабуецца Android 4.2 або вышэй, каб адключыць) + Прайграванне гуку пры фатаграфаванні Клавішы рэгулявання гучнасці Захаваць месца Тэчка для захавання фота / відэа файлаў Выкарыстоўваць сховішча Access Framework - Ці варта выкарыстоўваць сховішча Android 5 Access Framework для захавання фотаздымкаў і відэазапісаў + Ці варта выкарыстоўваць сховішча Access Framework для захавання фотаздымкаў і відэазапісаў Захаваць фотапрэфікс Выкарыстоўваць прэфікс для захавання фотаздымкаў Захаваць відэапрэфікс @@ -210,7 +210,7 @@ Ахвяруйце на распрацоўку Калі вам падабаецца гэты дадатак, калі ласка, падтрымайце распрацоўку . Вы можаце зрабіць гэта, купіўшы мой дадатак .Абярыце гэты параметр і адкрыецца старонка для ахвяравання . Дзякую! Выкарыстоўваць Camera2 API - Дазваляе выкарыстоўваць Camera2 API Android 5 + Дазваляе выкарыстоўваць Camera2 API Інфармацыя Інфармацыя Скід налад @@ -319,6 +319,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Не абмежавана Без затрымкі @@ -365,4 +372,8 @@ Інфармацыя пра камеру Умовы прадастаўлення паслуг + Выключана + Хаваць віртуальныя кнопкі навігацыі на экране + Хаваць GUI + Хаваць усе diff --git a/app/src/main/res/values-cs/arrays.xml b/app/src/main/res/values-cs/arrays.xml index 4ffc81cdd71e0d0255f69a44147bbb3c0cb9ed99..4cc0e8a6efd2fb47776d882421f20be8ebd966a8 100644 --- a/app/src/main/res/values-cs/arrays.xml +++ b/app/src/main/res/values-cs/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Vypnuto - Skrýt pouze virtuální navigační tlačítka - Skrýt grafické rozhraní - Skrýt vše - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Červená Zelená diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 2840ee91973363680a639f13952376f7ea27f3a4..b1c724848393c76de2f9715e08a324cc08438246 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -103,13 +103,13 @@ Pauza po pořízení fotografie Zobrazit fotografii po pořízení s možností sdílení nebo vymazání fotografie Zvuk závěrky - Přehrát zvuk při pořizování fotografie (vyžaduje Android 4.2 a vyšší pro vypnutí) + Přehrát zvuk při pořizování fotografie Tlačítka hlasitosti Uložit polohu Složka pro uložení fotek/videa Použít Storage Access Framework - Zdali použít Android 5\'s Storage Access Framework pro uložení fotek a videí + Zdali použít Storage Access Framework pro uložení fotek a videí Uložit předponu pro fotografie Předpona používaná při ukládání fotografií Uložit předponu pro videa @@ -214,7 +214,7 @@ Přispět na podporu vývoje Pokud se Vám tato aplikace líbí, prosím zvažte přispění koupí mé "darovací aplikace" - tato volba otevře stránku mé darovací aplikace. Díky! Použít Camera2 API - Zapne Camera2 API Androidu 5 - nabízí možnosti navíc, ale je v současné době experimentální (způsobí restart aplikace) + Zapne Camera2 API - nabízí možnosti navíc, ale je v současné době experimentální (způsobí restart aplikace) O programu Informace aplikace a ladění Obnovit nastavení @@ -323,6 +323,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Nekonečno Bez prodlení @@ -369,4 +376,8 @@ Informace o kameře Podmínky služby + Vypnuto + Skrýt pouze virtuální navigační tlačítka + Skrýt grafické rozhraní + Skrýt vše diff --git a/app/src/main/res/values-de/arrays.xml b/app/src/main/res/values-de/arrays.xml index b20ddf573a47df4bb5c3d90755394348a5902474..d1efcb09578937d27eec01311c92532d358335ce 100644 --- a/app/src/main/res/values-de/arrays.xml +++ b/app/src/main/res/values-de/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Nichts ausblenden - Nur Navigationstasten ausblenden - Alle Einstellelemente ausblenden - Alles ausblenden - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Rot Grün diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 096aff43ab3f9403b8b3791c31c38987f803d8bd..8007a256128bf8f31bfb970ca1b14f915cb82fbe 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -104,7 +104,7 @@ Pause nach der Fotoaufnahme Nach der Aufnahme pausieren mit der Möglichkeit, das Foto zu teilen oder zu löschen Auslöseton - Abspielen eines Tones bei der Aufnahme (benötigt Android 4.2 oder höher zum Deaktivieren) + Abspielen eines Tones bei der Aufnahme Lautstärketasten Audio Steuerungsoptionen Audio Steuerung Empfindlichkeit @@ -228,7 +228,7 @@ Spende Wenn Ihnen diese App gefällt, würde ich mich über eine Spende freuen, indem Sie meine Spenden-App kaufen. Dazu brauchen Sie nur diese Option anzuklicken. Danke! Camera2-API verwenden - Aktiviert die Android-5-Camera2-API mit Zusatzfunktionen (experimentell, Neustart erforderlich) + Aktiviert die Camera2-API mit Zusatzfunktionen (experimentell, Neustart erforderlich) Über Anwendungs- und Debug-Informationen Einstellungen zurücksetzen @@ -286,7 +286,6 @@ Expo {} Belichtungsreihe - []]] Schnelle Fotofolge NR @@ -548,6 +547,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Unbegrenzt Keine Verzögerung 0,5 s @@ -699,14 +705,16 @@ Zeige Zebrastreifen Zeigt in der Vorschau Zebrastreifen in den überbelichteten Stellen der Aufnahme.\n%s Aus - 70% - - 80% - - 90% - - 100% - + 70% + 80% + 90% + 93% + 95% + 97% + 98% + 99% + 100% + Fokus-Peaking Fokus-Peaking hebt scharf abgebildete Konturen hervor. Das ist vorrangig bei manuellem Fokus nützlich und hilft die scharf eingestellten Bereiche des Bilds zu erkennen.\n%s Aus @@ -730,7 +738,6 @@ Scan Schattierter Text Normaler Text - About Build version Die Kamera ist von OpenCamera gespalten Autoren @@ -770,10 +777,6 @@ Panorama wird beendet Bilder werden gespeichert… Text mit getöntem Hintergrund - 50x - 40x - 30x - 20x Hohe Qualität Schnell Minimal @@ -813,4 +816,12 @@ 4 3 2 - \ No newline at end of file + + + Wähle eine Fernsteuerung + + Nichts ausblenden + Nur Navigationstasten ausblenden + Alle Einstellelemente ausblenden + Alles ausblenden + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 85b3ad973acaf85b2d17ede8e1ea9f75a2571f26..2ff5bea0220c41ddb6451219c4451129dda1e6b7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -111,7 +111,7 @@ Χώρος αποθήκευσης Φάκελος για αποθήκευση φωτογραφίας/βίντεο Χρήση αποθήκευσης πρόσβασης πλαισίου - Αν θέλετε χρησιμοποιήστε την Android 5\'s Πρόσβαση αποθήκευσης πλαισίου για την αποθήκευση φωτογραφιών και βίντεο + Αν θέλετε χρησιμοποιήστε την Πρόσβαση αποθήκευσης πλαισίου για την αποθήκευση φωτογραφιών και βίντεο Αποθήκευση προθέματος φωτογραφιών Πρόθεμα για την αποθήκευση αρχείων φωτογραφιών Αποθήκευση βίντεο προθέματος @@ -227,7 +227,7 @@ Δωρεά για υποστήριξη της ανάπτυξης Αν σας αρέσει αυτή η εφαρμογή, παρακαλούμε κάντε μια δωρεά για υποστήριξη της ανάπτυξης. Μπορείτε να το κάνετε αυτό μέσω της επιλογής "Δωρεά" - κάντε αυτή την επιλογή για να ανοίξει η σελίδα δωρεών. Ευχαριστούμε! Χρήση τού Camera2 API - Ενεργοποίηση του Camera2 API σε συσκεύες Android 5 - προσφέρει επιπλέον χαρακτηριστικά, αλλά ενδέχεται να μην λειτουργούν σωστά σε όλες τις συσκευές (θα προκαλέσει μια επανεκκίνηση) + Ενεργοποίηση του Camera2 API - προσφέρει επιπλέον χαρακτηριστικά, αλλά ενδέχεται να μην λειτουργούν σωστά σε όλες τις συσκευές (θα προκαλέσει μια επανεκκίνηση) Σχετικά με Εφαρμογή και πληροφορίες εντοπισμού σφαλμάτων Ρυθμίσεις επαναφοράς diff --git a/app/src/main/res/values-es/arrays.xml b/app/src/main/res/values-es/arrays.xml index a8c790100c40b4a0140dd55815fe71c1ff0397bd..2737fac05945b777b5f8059cc23b40c4f0a98f97 100644 --- a/app/src/main/res/values-es/arrays.xml +++ b/app/src/main/res/values-es/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Apagado - Sólo ocultar los botones de navegación en pantalla - Ocultar interfaz de usuario - Ocultar todo - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Rojo Verde diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3d169ca3fe4521c56d4b7663b502d4ee5e70d7eb..77e383097ac60724d2ea3db60543f65895957e8c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -112,7 +112,7 @@ Carpeta de almacenamiento Carpeta para guardar los archivos de foto/video Utilizar el Framework de Acceso del Almacenamiento - Utilice el Framework de Acceso del Almacenamiento de Android 5 para guardar fotos y videos + Utilice el Framework de Acceso del Almacenamiento para guardar fotos y videos Prefijo de foto Prefijo utilizado para el nombre de las fotos Prefijo de video @@ -228,7 +228,7 @@ Done para apoyar el proyecto Si te gusta esta aplicación, por favor considera hacer una donación para apoyar el proyecto. Puedes hacerlo comprando mi "aplicación de donación" - haz clic en esta opción para abrir la página de mi aplicación de donación. ¡Gracias! Utiliza Camera2 API - Utiliza la Camara2 de Android 5 - ofrece funcionalidades extra, pero esta en fase experimental (causara un reseteo de la aplicación) + Utiliza la Camara2 - ofrece funcionalidades extra, pero esta en fase experimental (causara un reseteo de la aplicación) Acerca de Información de aplicación y depuración Restablecer ajustes @@ -292,7 +292,6 @@ HDR BKT {} Horquillado de exposición - []]] Ráfaga rápida NR Reducción de ruido @@ -539,6 +538,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Ilimitado Sin retardo @@ -600,4 +606,8 @@ Información de la cámara Términos de servicio + Apagado + Sólo ocultar los botones de navegación en pantalla + Ocultar interfaz de usuario + Ocultar todo diff --git a/app/src/main/res/values-fr/arrays.xml b/app/src/main/res/values-fr/arrays.xml index 21e492c626e21587dbc6e894503010aa40d5d9e4..121eab85c5fb0a8d15ceab59968b1d4e174c39c5 100644 --- a/app/src/main/res/values-fr/arrays.xml +++ b/app/src/main/res/values-fr/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Off - Masquer uniquement les boutons de navigation virtuelle de l\'écran - Masquer l\'interface graphique - Tout masquer - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Rouge Vert diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 31ca68864996c2b58738693ba993f54e76d6cdee..ef67fa2647f4a456b4eef9d5e4356ce12fec6942 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -101,7 +101,7 @@ Mettre en pause après la prise de photo Mettre en pause l\'écran après avoir pris une photo, avec la possibilité de partager ou supprimer la photo Son de l\'obturateur - Jouer un son lorsque vous prenez une photo (nécessite Android 4.2 ou supérieur pour désactiver) + Jouer un son lorsque vous prenez une photo Touches de volumes Option contrôle audio Sensibilité contrôle audio @@ -375,6 +375,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Illimité Pas de délai 1 s @@ -421,7 +428,6 @@ Interface utilisateur pour droitier Texte ombragé Texte brut - About Version Camera est dérivée d\'OpenCamera Auteurs @@ -657,10 +663,6 @@ 5 Go 2 Go 0,5 s - 50x - 40x - 30x - 20x Précédent 4x 2x @@ -856,4 +858,8 @@ Expo {} Impossible de sauvegarder dans ce dossier Suivant + Off + Masquer uniquement les boutons de navigation virtuelle de l\'écran + Masquer l\'interface graphique + Tout masquer \ No newline at end of file diff --git a/app/src/main/res/values-hu/arrays.xml b/app/src/main/res/values-hu/arrays.xml index 6eafb5d23dfca3a39c760ea69b778816c31fac73..62be77f76de179a6869a9a8b6539de38010e0eb3 100644 --- a/app/src/main/res/values-hu/arrays.xml +++ b/app/src/main/res/values-hu/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Ki - Csak a virtuális navigációs gombok elrejtése - Kezelőfelület elrejtése - Minden elrejtése - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Piros Zöld diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 87263e38f064b4805bdd0dfd8ab2dca97259f144..170b51703cae2954f38c530e7bccfe2136602f08 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -112,7 +112,7 @@ Mentés helye Fotók, videofájlok mentésének könyvtára Storage Access Framework használata - Használja-e az Android 5 Storage Access Frameworkjét a fotók és videók mentéséhez + Használja-e az Storage Access Frameworkjét a fotók és videók mentéséhez Fotó fájlnév előtag A fényképek mentésekor használt fájlnév előtag Videó fájlnév előtag @@ -228,7 +228,7 @@ Támogasd a fejlesztést adománnyal A tetszik az alkalmazás, fontold meg adomány küldését a fejlesztés támogatására. Ezt a „támogatói alkalmazás” megvásárlásával teheted meg – kattints ide, hogy megnyisd a támogató alkalmazás oldalát. Köszönjük! Camera2 API használata - Lehetővé teszi az Android 5-ös Camera2 API használatát - extra funkciókat kínál, de nem minden eszközön működik megfelelően (újraindulást eredményez) + Lehetővé teszi az Camera2 API használatát - extra funkciókat kínál, de nem minden eszközön működik megfelelően (újraindulást eredményez) Névjegy App és hibakeresési információk Beállítások visszaállítása @@ -444,6 +444,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Korlátlan Nincs késleltetés @@ -505,4 +512,8 @@ Kamera információk Szolgáltatási feltételek + Ki + Csak a virtuális navigációs gombok elrejtése + Kezelőfelület elrejtése + Minden elrejtése diff --git a/app/src/main/res/values-it/arrays.xml b/app/src/main/res/values-it/arrays.xml index 9fa703dbdc6ff7044e32b6357e3380402c07fe92..abdeb6efcb89659843e07e112a188c8e2657cfba 100644 --- a/app/src/main/res/values-it/arrays.xml +++ b/app/src/main/res/values-it/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Off - Nascondi solamente i bottoni di navigazione virtuali on-screen - Nascondi GUI - Nascondi tutto - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Rosso Verde diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index fdd67ddf2a227e70fb56c35eade25b637d0048a0..edf0b79fa95a0005f6dd4e28ec193d8df62c198a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -105,7 +105,7 @@ Pausa dopo aver scattato Mette in pausa lo schermo dopo aver scattato, permettendo inoltre di condividere o cancellare la foto Suono otturatore - Emette un suono mentre scatta la foto + Emette un suono quando scatta una foto Tasti volume Opzioni di controllo del suono Sensibilità di controllo del suono @@ -236,7 +236,7 @@ Dona per aiutare lo sviluppo Se questa app ti piace, puoi sempre considerare di fare una donazione per aiutarne lo sviluppo. Puoi farlo acquistando l\'app my donation - tocca questa scelta per aprire la pagina dell\'app my donation. Grazie mille! Usa le API Camera2 - Abilita funzionalità extra, come modalità manuali per esposizione, messa a fuoco, bilanciamento del bianco, e anche RAW (se supportate dal dispositivo), ma potrebbe non funzionare correttamente su tutti i modelli (potrebbe causarne il riavvio) + Abilita API Camera2 - offre caratteristiche extra, ma potrebbe non funzionare correttamente su tutti i modelli (potrebbe causare un riavvio) Informazioni App e informazioni di debug Ripristina le impostazioni @@ -471,6 +471,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Illimitato Nessun intervallo 1s @@ -517,7 +524,6 @@ Interfaccia utente destrorsi Testo ombreggiato Testo piano - About Versione build La telecamera è biforcuta da OpenCamera Autori @@ -530,10 +536,6 @@ Se eseguire o meno l\'auto-focus all\'avvio di Fotocamera. Se riscontri il problema del flash che si accende all\'avvio, disabilita questa opzione Non salvare le immagini originali 0.5s - 50x - 40x - 30x - 20x Precedente Aumenta o diminuisce la compensazione dell\'esposizione 4x @@ -858,4 +860,9 @@ whether a setting is turned on, e.g., \"Auto-stabilise: On\" Fotocamera utilizza icone dal Google\'s Material Design icons (https://developer.android.com/design/downloads/index.html , https://design.google.com/icons/ , https://github.com/google/material-design-icons/ , https://google.github.io/material-design-icons/) rilasciate sotto la licenza Apache versione 2.0. Alcune icone sono state modificate. Tocca qui per il testo completo della licenza. whether a setting is turned on, e.g., \"Auto-stabilise: On\" - \ No newline at end of file + + Off + Nascondi solamente i bottoni di navigazione virtuali on-screen + Nascondi GUI + Nascondi tutto + diff --git a/app/src/main/res/values-ja/arrays.xml b/app/src/main/res/values-ja/arrays.xml index 712abb7f829dbb58da6fb019020b0737315646ac..296b7ef75fd101cad68016f2306c3a0204f34e93 100644 --- a/app/src/main/res/values-ja/arrays.xml +++ b/app/src/main/res/values-ja/arrays.xml @@ -1,15 +1,15 @@ - 自動 + オートフォーカス 無限遠 マクロ + 固定(タッチAF) + マニュアルフォーカス 固定 - マニュアル - Fixed EDOF - コンティニュアス - コンティニュアス + コンティニュアスAF + コンティニュアスAF focus_mode_auto @@ -61,8 +61,8 @@ 300 - 最大サイズ - マッチしたサイズ (WYSIWYG) + ディスプレイに合わせる + 撮影範囲に合わせる(WYSIWYG) preference_preview_size_display @@ -70,8 +70,8 @@ なし - 1回押し - 2回押し + タッチ + ダブルタップ none @@ -81,9 +81,9 @@ 写真を撮る (ビデオ録画の開始/終了) フォーカス - ズームの拡大/縮小 + ズームイン/ズームアウト 露出補正 - 自動手振れ防止のオン/オフ + 傾き補正のオン/オフ 音量の変更 なし @@ -104,18 +104,6 @@ 0 180 - - オフ - スクリーンボタンのみ除外 - GUIを除外 - すべてを除外 - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - @@ -254,4 +242,59 @@ audio_mono audio_stereo + + なし + 音に反応 + 音声コマンド: \"チーズ\" + + + none + noise + voice + + + ローカルタイム + UTC協定世界時 + + + local + zulu + + + 3枚 + 5枚 + + + 3 + 5 + + + ±1/2EV + ±1EV + ±2EV + ±3EV + + + 0.5 + 1 + 2 + 3 + + + 反転しない + 反転する + + + preference_front_camera_mirror_no + preference_front_camera_mirror_photo + + + 記録しない + 記録する + + + preference_video_subtitle_no + preference_video_subtitle_yes + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e2ec5d49895d949959e68eba7e8292bdcbd0c2d9..fe25856d14ccd3f78dd43274d0a175a7fae89428 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -6,8 +6,8 @@ ポップアップ設定 了解しました (この内容を再度表示しない) - 有効 - 無効 + オン + オフ 保存場所を選択する: フォルダの履歴を消去する @@ -18,7 +18,7 @@ 繰り返します カメラへの接続に失敗しました ビデオファイルが壊れている可能性があります - この機械はサポートできません + が無効です 不明なエラーです、ビデオを停止します サーバーが反応しません、ビデオを停止します 最大撮影時間に達しました @@ -31,8 +31,8 @@ リアカメラ 写真 ビデオ - 露出を設定しました - 露出を解除しました + 露出を固定しました + 露出の固定を解除しました タイマーを解除しました 高速撮影を解除しました タイマーを開始しました @@ -41,7 +41,8 @@ 録画を開始しました すみません 保存に失敗しました - 回転に失敗しました + RAWファイルの保存に失敗しました + 傾き補正に失敗しました プレビューに失敗しました 写真を撮ります 撮影に失敗しました @@ -50,7 +51,7 @@ ギャラリーアプリがありません 画面を固定しました。スワイプで解除します。 解除 - すみませんがこの機械では自動回転が使えません + すみませんがこの端末では傾き補正が使えません 音声は無効です 最大撮影時間 カラーエフェクト @@ -68,12 +69,12 @@ [固定: スワイプで解除] - 情報の貼り付けに失敗しました + スタンプの貼り付けに失敗しました m カメラエフェクト - 自動回転 - 水平を(写真のみ)自動的に修正します(撮影後の処理に時間がかかります。メモリの少ない機械では失敗する事があります) + 傾き補正 + 水平を(写真のみ)自動的に修正します(撮影後の処理に時間がかかります。メモリの少ない端末では失敗する事があります) カラーエフェクト 選択した色による効果を適用します シーンモード @@ -81,66 +82,71 @@ ホワイトバランス ホワイトバランスを選択します ISO感度 - 高い感度を設定すると僅かな光でも撮影できます (一部動作しない機械もあります) + 高い感度を設定すると僅かな光でも撮影できます (一部動作しない端末もあります) 露出補正 0に設定するとデフォルトの露出補正になります 写真/ビデオの向き - 設定すると機械上の向きよりも設定された向きを優先して保存します\n%s - 顔検出 - フォーカスの設定に顔検出を利用します + 設定すると端末の向きよりも設定された向きを優先して保存します\n%s + 顔認識 + フォーカスエリアの代わりに顔認識を利用します カメラ制御 タイマー タイマー音 - タイマーの計測もしくは高速撮影時の間隔の計測で音を鳴らします + タイマー撮影やインターバル撮影のカウントダウンで音を鳴らします 音声タイマー - タイマーの計測もしくは高速撮影時の間隔の計測で(残り1分から)音声を流します - 高速撮影 - 撮影間隔 + タイマー撮影やインターバル撮影のカウントダウンで(残り1分から)音声を流します + 連続撮影 + インターバル カメラ制御の詳細設定… - タッチして保存する - タッチもしくはプレビューを2回タップして写真を保存します + 画面タッチで撮影する + 画面をタッチまたはダブルタップして写真を撮影します 撮影の後に一時停止する 一時停止した画面上で写真の共有もしくは削除ができます シャッター音 - 撮影時に音を再生する (無効にする場合はAndroid 4.2以上が必要です) + 撮影時に音を再生します 音量キー - + 音声コントロール設定 + + 音声コントロール感度 + 音に対する感度の設定 保存場所 写真/ビデオを保存するフォルダを決めます ストレージへのフレームワークを使用する - 写真やビデオを保存する際にAndroid 5.0以降のフレームワークを使用するかどうかを選択します + 写真やビデオを保存する際にStorage Accessフレームワークを使用するかどうかを選択します。外付けのSDカードに保存する場合はこの設定が必要となります。 保存する写真の命名ルール 保存する写真のファイル名の手前につける内容を設定します 保存するビデオの命名ルール 保存するビデオのファイル名の手前につける内容を設定します + ファイル名に使用する時刻フォーマット ロック画面上でもカメラを表示する - 有効にするとOpen Cameraがロック画面上でも表示されます(ギャラリーを規定のアプリにしているなど、ロックの解除が必要になる場合もあります) - + 有効にするとOpen Cameraがロック画面上でも表示されます(ギャラリーを既定のアプリにしているなど、ロックの解除が必要になる場合もあります) + 起動時にオートフォーカスを行う + Open Camera の起動時にオートフォーカスを行います。もし起動時にライトが点灯する問題が起こる場合は設定をオフにしてください。 録画中は画面を固定する 録画が停止しないように画面を固定します。固定は画面をスワイプすると解除されます。アプリをバックグラウンドに移すか画面を消灯しても録画は停止します。 - 回転をプレビュー - プレビューを回転して表示します(写真/ビデオの保存には影響しません)\n%s - 画面上での表示設定… + プレビューを回転 + プレビューを180度回転して表示します(写真/ビデオの保存には影響しません)\n%s + 画面表示設定… プレビューサイズ - 操作位置 + ボタン配置 全画面表示モード - ズームを表示する - 画面上にズームレベルを表示します(ズームインする場合) - ズーム制御を表示する + ズーム倍率を表示する + 画面上にズーム倍率を表示します(ズームインする場合) + ズームボタンを表示する 画面上にズーム制御の -/+ ボタンを表示します ズーム制御のスライダーを表示する 画面上にズーム制御のスライダーを表示します ISOを表示する - 画面上にISO感度の状態を表示します (Camera2 APIが必要です) + 画面上にISO感度を表示します。自動発光モードの場合は発光するかどうかも表示します。 (Camera2 APIが必要です) 空きメモリを表示する 画面上に空きメモリを表示します 角度を表示する 画面上に角度を表示します 角度を線で表示する 画面上に線を表示します - 角度を色で表示する - 角度の具合を色で表示します + 角度のハイライトカラー + カメラがほぼ水平になった時のハイライトカラーを指定します 方角を表示する 画面上に方角を表示します 時間を表示する @@ -155,6 +161,8 @@ 画面上のメッセージをトースト形式で表示します サムネイルをアニメにする 撮影時のサムネイルをアニメにして表示します + 写真撮影中に枠線を表示する + 写真の撮影中に枠線を表示します 表示を維持する 有効にすると使用している間は画面が消えなくなります 明るさを常に最大にする @@ -166,55 +174,60 @@ 位置情報の設定… 写真の解像度 写真の品質 - 保存する写真の品質を決めます (90%%の圧縮率を提案します)\n%s + JPEG形式とWebP形式の保存品質を設定します(初期値は90%%)。PNG形式には影響しません。\n%s + RAW 位置情報を使用する - 写真/ビデオにジオタグを埋め込みます + 写真/ビデオにジオタグを埋め込みます(写真の場合、JPEGまたはDNGの場合のみ埋め込み可能) 方角情報を使用する - 写真にコンパスを埋め込みます + 写真にコンパスを埋め込みます(JPEG形式のみ) 位置情報を要求する 有効にすると位置情報を取得可能な場合のみ写真/ビデオの撮影が動作します - 情報を貼り付ける + スタンプを貼り付ける 写真に日付と時刻を貼り付けます。GPSが有効であれば位置情報も埋め込みます。\n%s 日付の形式 時刻の形式 GPSの形式 文字の内容 - 写真に貼り付ける文字の内容を決めます + 写真に貼り付けるスタンプ文字の内容を決めます 文字のサイズ - 写真に貼り付ける文字の大きさを決めます + 写真に貼り付けるスタンプ文字の大きさを決めます 文字の色 - 写真に貼り付ける文字の色を決めます - 文字の形状 - 写真に貼り付ける文字の形状を決めます\n%s + 写真に貼り付けるスタンプ文字の色を決めます + 文字の装飾 + 写真に貼り付けるスタンプ文字の装飾方法を決めます\n%s ビデオの解像度 - 4K UHDビデオ (実験中) - 3840x2160での撮影を有効にします - サポートしていない機械ではクラッシュする事もあります! - 自動回転を有効にする - ビデオ撮影時の自動回転を設定します - ビデオのビットレート - ビデオのビットレートを設定します(設定を高くするとより多くの保存スペースが必要になり、サポートしていないレートを設定すると録画に失敗する事があります)\n%s + 4K UHD ビデオ(一部端末のみ) + リアカメラによる3840x2160での撮影を有効にします - これは端末メーカーが公開していない4Kサポート機能を使って4K撮影を可能にします。動作保証はありませんので、利用する前に必ずテストを行ってください。 + 手ぶれ補正を有効にする + ビデオ撮影時の手ぶれ補正を設定します + ビデオのビットレート(目安) + ビデオのビットレートを設定します(設定を高くすると品質は良くなりますが、より多くの保存スペースが必要になります。サポートしていないレートを設定すると録画に失敗する事があります)\n%s ビデオのフレームレート - ビデオのフレームレート(FPS)を設定します(サポートしていないレートを設定すると録画に失敗する事があります)\n%s + ビデオのフレームレート(FPS)を設定します(あくまで目安のため、保証はされません。サポートしていないレートを設定すると録画に失敗する事があります。)\n%s ビデオの最大撮影時間 指定した時間が経過するとビデオ撮影が自動的に停止します\n%s ビデオの最大撮影回数 - 最大撮影時間の経過後に指定した回数だけ撮影を繰り返します(有効時のみ)\n%s - + 最大撮影時間が経過すると録画は停止しますが、このオプションでは指定した回数まで撮影を自動で再開します。\n%s + ビデオの最大ファイルサイズ + ビデオ撮影は最大ファイルサイズに到達すると停止または停止&再開(以下のオプションを参照)します。注意:多くのAndroid端末には端末自体に最大ファイルサイズ制限(2GBや4GBなど)が設定されていますが、このオプションでは任意の値を設定できます。注意:このオプションでは端末自体の最大ファイルサイズ制限を増やすことはできません。\n%s + 最大ファイルサイズで停止&再開 + 最大ファイルサイズに到達したら自動的に録画の停止と再開をします。(端末のファイルサイズ制限かユーザーが指定した任意のサイズのどちらか小さい方) 音声録音 ビデオの撮影時に音声も録音します オーディオソース - 音声の録音にマイクを使用します\n%s + 録音に使用するマイク\n%s オーディオチャンネル - モノラルもしくはステレオを指定します (ステレオはサポートしている機械でのみ有効です) - 録画中にフラッシュを使用する - 有効にすると撮影中にフラッシュを使う事ができます (撮影中である事を相手に伝えます) + モノラルもしくはステレオを指定します (ステレオはサポートしている端末でのみ有効です) + 録画中にライトを点滅する + 有効にすると撮影中にライトが点滅します (撮影中である事を相手に伝えることができます) その他の内容 オンラインヘルプ + Open Camera のWEBサイトをブラウザで開きます サポートのために寄付をする このアプリが気に入りましたら寄付をお願いします。この項目を押すと寄付への窓口が開きます。寄付権を購入する事で寄付ができます。寄付してくれたらありがとう! Camera2 APIを使う - 有効にするとAndroid 5.0以降に搭載のCamera2 APIを使用します。この設定は試験的に提供しています (再起動が必要です) + 有効にするとCamera2 APIを使用します。この設定は試験的に提供しています (再起動が必要です) このアプリについて アプリとデバッグの情報を表示します 設定をリセットする @@ -231,6 +244,10 @@ 露出を固定する フォーカス切替 フラッシュ切替 + フロントカメラ切替 + リアカメラ切替 + ビデオモードに切り替える + 写真モードに切り替える さっきの写真を削除する この写真を共有する @@ -244,11 +261,99 @@ 保存先を選択する ストレージへのアクセスがキャンセルされました - + このフォルダには保存できません + マイクの使用が許可されていません 位置情報の使用が許可されていません + ビデオ撮影を開始する + ビデオ撮影を停止する + + MB + GB + + 音声コントロールのリスニングを開始する + 音声コントロールのリスニングを停止する + \"チーズ\"と言ってください + 何か大きな音を立ててください + + 傾き補正は写真が水平になるよう自動的に角度を調整します。\n\n注意:これには回転と切り抜きの処理が必要になるため、解像度と画角(写る範囲)が小さくなります。 + HDR(ハイダイナミックレンジ)は被写体の輝度差が大きな場面で役立ちます。異なる露出で複数の撮影を行い、それらを合成して画像を生成します。\n\n注意:HDRは動きの速い場面には適さないため、色が正しく表現されない可能性があります。\n\nHDRモードは通常の撮影と比べて時間がかかります。 + + 再度表示しない + + 撮影モード + 標準 + 標準 + HDR + 露出BKT + 露出ブラケット + ハイスピード連写 + NR + ノイズリダクション撮影 + スタンプ + HDR撮影時すべての写真を保存する + これを有効にするとHDR撮影時に3つの異なる露出の写真がHDR写真と共に保存されます。注意:スタンプや傾き補正のようなオプションを使用している場合は保存が遅くなります。 + + カメラの権限がありません + 権限が必要です + カメラを有効にするにはカメラの権限が必要です + 写真を保存するにはストレージの読み書き権限が必要です + 音声付き動画の撮影と音声コントロールを使用するにはマイクの権限が必要です + ジオタギング(位置情報を写真やビデオに付加する)には位置情報の権限が必要です。 + 位置情報の権限はBluetooth LE リモートコントロールデバイスの接続にも必要です。 + + デバッグ用オプション + デバッグ用オプション + 代替のフラッシュ方式を使用 + Camera2 API 使用中に端末のフラッシュの挙動が不安定な場合は有効にしてください。 + + 露出ブラケット + 露出ブラケットモードで撮影する枚数を指定します。\n%s + 露出ブラケット幅 + 変化させる露出の幅を指定します\n%s + + ノイズリダクション 元画像保存 + ノイズリダクション撮影時、元の画像を保存します。注意:写真の保存には時間がかかります。\n%s + + 元画像を保存しない + 単一の元画像を保存する + 全ての元画像を保存する(遅い) + + + + ビデオを記録するための充分な空きスペースがありません + ビデオ撮影を停止しました\nバッテリー残量が非常に少なくなりました + バッテリー残量チェック + バッテリー残量が非常に少なくなった場合に録画を停止します。これにより、端末の電源が落ちてファイルが破損するリスクを軽減します。 + + 高速なHDR/露出BKT撮影 + 高速なHDR/露出ブラケット撮影を行います。 もしHDR/露出ブラケット撮影で問題が起こる場合はこの設定を無効にしてください。 + + ビデオ撮影 + セルフィー + + フロントカメラを反転 + フロントカメラ撮影時に左右反転した画像を保存します。\n%s + + 加速度センサーの校正 + このオプションは端末の加速度センサーを校正します。これにより傾き補正と角度表示が正確に動作するようになります。 + 端末を縦向きまたは横向きに平らな場所へ置いて水平になった状態で校正を選択します。\n\n設定を初期値に戻すにはリセットを選択します。\n\nキャンセルするには戻るボタンを押します。 + 校正 + リセット + 加速度センサーを校正しました + 加速度センサーを初期値に戻しました + + DRO + + 仰角/俯角を線で表示する + 仰角/俯角を水平な線で表示します + 方角を線で表示する + 方角を線で表示します + ビデオサブタイトル + 日付情報と時刻情報、もしGPSが有効であればGPS情報も格納されたサブタイトル(.SRT)が作成されます。\n%s + 制限なし 3秒 5秒 @@ -273,19 +378,156 @@ 30分 45分 1時間 + + シャッターボタンを表示する + 撮影と録画のボタンを表示します。他の方法で撮影したい場合はチェックを外します(シャッター専用ボタンが付いている、音量ボタンを使用するなど) + + 撮影中… + + リスニングの初期化に失敗しました + HDR画像の作成に失敗しました + + オート + 曇り + 太陽光 + 蛍光灯 + 白熱電球 + 日陰 + トワイライト + 温白色 + マニュアル + + アクション + バーコード + 砂浜 + ろうそく + オート + 花火 + 風景 + 夜景 + 人物(夜) + パーティー + 人物 + 雪景色 + スポーツ + 夕焼け + シアター + + 水中 + 黒板 + モノクロ + 反転 + なし + ポスタリゼーション + セピア + ソラリゼーション + ホワイトボード + + 新着情報: + 寄付 + + + 人の顔 + 人の顔 + が画面の左側にあります + が画面の右側にあります + が画面の上部にあります + が画面の下部にあります + が画面の中央にあります + デフォルト + カスタムEXIFタグ + アーティスト + EXIFデータ内の\"Artist\"タグに記録されます。(JPEG形式のみ)(使用しない場合は空にします) + 著作権表記 + EXIFデータ内の\"Copyright\"タグに記録されます。(JPEG形式のみ)(使用しない場合は空にします) + + 処理中…あと + デフォルト 年-月-日 日/月/年 月/日/年 なし - オフ - 自動 - オン - 懐中電灯 + HS撮影枚数 + 2枚 + 3枚 + 4枚 + 5枚 + 6枚 + 8枚 + 10枚 + 12枚 + 15枚 + 20枚 + + BKT撮影枚数 + 2枚 + 3枚 + 4枚 + 5枚 + 6枚 + 8枚 + 10枚 + 12枚 + 15枚 + 20枚 + 25枚 + 30枚 + 40枚 + 50枚 + 100枚 + 150枚 + 200枚 + + タイムラプス + なし + + 発光禁止 + 自動発光 + 強制発光 + 常時発光 赤目軽減 + 画面発光 自動 + 画面発光 オン + 画面発光 常時 + + フリッカー軽減 + 照明器具のフリッカー(ちらつき)を軽減します。\n%s + 自動 + 50Hz(東日本) + 60Hz(西日本) + オフ + + ゴーストを表示 + 撮影の位置合わせを容易にするために画像を重ねて表示します。\n%s + 表示しない + 最後に撮った写真を使用 + 選択された画像を使用 + + この端末にはファイル選択ダイアログが無いため、ゴーストの表示には非対応です + この画像は開けません + + ビデオ撮影中の写真撮影 + ビデオ撮影中の写真撮影を許可します。もし端末の Camera2 API の録画が不安定な場合はこの設定を無効にしてください。 + + ビデオのフラットプロファイル(Log) + ビデオのフラットプロファイル(Log)を有効にします\n%s + オフ + ファイン + + + + 最強 + + ビデオカメラ + 外部マイク(接続されている場合) + デフォルトのオーディオソース + 通話用に最適化 + 音声認識用に最適化 + 音声処理を行わない デフォルト 100kbps @@ -311,16 +553,71 @@ 80Mbps 90Mbps 100Mbps + 150Mbps + 200Mbps + + オーディオレベルを表示する + 動画撮影中にオーディオレベルを表示します + + フォーカスBKT + フォーカスブラケット + + フォーカスブラケット 手前側距離 + フォーカスブラケット 奥側距離 + + 無限遠を追加 + + コンパスの向き + 端末のコンパス精度を改善するためにキャリブレーションが必要です。これは端末を8の字に動かすことで実行できます。\n\n現在の精度: + + 距離の単位 + スタンプとビデオサブタイトルのGPS高度に使用されます。\n%s + メートル + フィート + ft + + お知らせを表示する + このアプリが更新された時に新機能や改良点のお知らせを表示する + + 輪郭強調アルゴリズム + カメラが輪郭強調に使用するアルゴリズムを選択します。輪郭強調は写真の精細さを向上させます。(この設定はNR撮影時は無視されます)\n%s + + プロセッシング設定… + + ノイズリダクションアルゴリズム + カメラがノイズリダクションに使用するアルゴリズムを選択します。ノイズリダクションは特に暗い場所での撮影時に発生する多くのノイズを除去することで写真の品質を向上させます。(これはNR撮影モードとは独立しています。この設定はNR撮影モードでは無視されます。)\n%s + デフォルト + オフ + 最小限 + 高速 + 高品質 + + フォーカス補助 + マニュアルフォーカス操作中に画面を拡大表示します\n%s + オフ + 2倍 + 4倍 + + 前の値に変更 + 次の値に変更 なし - 2倍 - 3倍 - 4倍 - 5倍 - 10倍 + 2枚 + 3枚 + 4枚 + 5枚 + 10枚 + 20枚 + 30枚 + 40枚 + 50枚 + 100枚 + 200枚 + 500枚 無制限 なし + 0.5秒 1秒 2秒 3秒 @@ -338,25 +635,180 @@ 1時間 2時間 + 端末のデフォルト + 100MB + 200MB + 300MB + 500MB + 1GB + 2GB + 5GB + 9GB + なし - 1.00 (1対1) - 1.25 (5対4) - 1.33 (4対3) - 1.4 (7対4) - 1.5 (3対2) - 1.78 (16対9) - 1.85 (37対20) - 2.00 (2対1) - 2.33 (21対9) - 2.35 (47対20) - 2.4 (12対5) + 1 (1:1) + 1.25 (5:4) + 1.33 (4:3) + 1.4 (7:4) + 1.5 (3:2) + 1.78 (16:9) + 1.85 (37:20) + 2 (2:1) + 2.33 (21:9) + 2.35 (47:20) + 2.4 (12:5) + + +3 (高感度) + +2 + +1 + 0 (初期値) + -1 + -2 + -3 (低感度) + + HDRコントラスト強調 + HDR撮影時にコントラスト強調アルゴリズムを使用します。大きな輝度差があるようなシーンでの見た目を改善し、いわゆる「HDRルック」な写真が撮影されます。\n%s + オフ + スマート + 常時 + + ビデオの保存形式 + ビデオとオーディオの形式\n%s + + デフォルト + MPEG4 H264 + MPEG4 HEVC + 3GPP + WebM (オーディオには非対応) + + 設定マネージャー + + 設定を保存 + Open Camera のすべての設定をファイルに保存します + 保存する設定の名前 + + 設定を復元 + 以前保存した設定を復元します。この操作を行うと現在の全ての設定が上書きされます! + このオプションは以前保存した設定ファイルのうち1つを選択できます。復元するファイルを選択すると現在の設定は全て上書きされます! + + 設定を保存しました + 設定の保存に失敗しました + 設定の復元に失敗しました + + 写真の保存形式 + JPEG + WebP + PNG + + NRモード + 標準 + ローライト 左利き用 右利き用 + 画面上部 (縦表示時) + + 顔認識アイコンを表示する + 顔認識のオンオフを切り替えるアイコンを表示します + 顔認識を有効にする + 顔認識を無効にする + 顔認識オン + 顔認識オフ + + 傾き補正アイコンを表示する + 傾き補正のオンオフを切り替えるアイコンを表示します。傾き補正を有効にすると写真は水平になるよう自動的に補正されます。 + 傾き補正オン + 傾き補正オフ + + スタンプアイコンを表示する + スタンプのオンオフを切り替えるアイコンを表示します + スタンプを有効にする + スタンプを無効にする + スタンプ オン + スタンプ オフ + + カスタムテキストスタンプアイコンを表示する + カスタムテキストスタンプを入力するためのアイコンを表示します + + ホワイトバランス固定アイコンを表示する + ホワイトバランスを固定/固定解除するアイコンを表示します + ホワイトバランスを固定する + ホワイトバランスの固定を解除する + ホワイトバランスを固定しました + ホワイトバランスの固定を解除しました + + 露出固定アイコンを表示する + 露出を固定/固定解除するアイコンを表示します + 露出の固定を解除する + + ローライトモード: 端末をしっかりと押さえてください + + パノラマ + パノラマ - 影付き文字 + Bluetooth LE リモートコントロール… + Bluetooth LE リモートコントロールを有効にする + Bluetooth LE (BLE) リモートコントロールデバイスを有効にする + リモートデバイスタイプ + + BLE をサポートしていません。 + BLE デバイススキャン + Bluetooth をサポートしていません。 + 不明なデバイス + 接続先から切断された場合は画面を暗くする + ヒント: Open Camera の起動時にデフォルトの輝度を最小輝度に設定します。 + 深度計算のために海水を使用する + 水中ハウジングの場合、正しい水のタイプを選択すると精度が向上します。 + 接続されています + + 住所の使用 + + もし可能であればGPS座標だけでなく住所も表示する。 + 住所を優先して表示する(取得できない場合はGPS座標) + 住所を表示しない + + 位置情報記録アイコンを表示する + 位置情報記録のオンオフを切り替えるアイコンを表示します(ジオタギング) + 位置情報を記録する + 位置情報を記録しない + + ヒストグラムを表示 + 色のヒストグラムを表示します。\n%s + オフ + RGB表示 + 輝度(RGBの加重平均) + Value(RGBのうち最大値) + Intensity(RGBの平均) + Lightness(RGBのうち最大値と最小値の平均) + + ゼブラ模様を表示 + このオプションを有効にすると、露出オーバーになっている部分に縞模様が表示されます。\n%s + Off + 70% + 80% + 90% + 100% + + フォーカスピーキング + このオプションを有効にするとピントの合った輪郭部分がハイライト表示されます。 これは主にマニュアルフォーカス時どこにピントが合っているのか判断するのに役立ちます。\n%s + オフ + オン + フォーカスピーキングの色 + ピントの合った輪郭部分のハイライト色を設定します\n%s + + + + プレビュー画面設定… + + フラッシュ切替 + フラッシュアイコンを表示する + フラッシュの切り替えをするボタンをポップアップメニューの代わりに表示します + + 現在の接続先: + スキャン + + 文字を縁取りする 普通の文字 - About ビルドバージョン カメラはOpenCameraから分岐します 作者 @@ -364,5 +816,56 @@ 免許 カメラ情報 利用規約 + 文字の背景を少し暗くする + + パノラマ撮影を終了する + パノラマ撮影をキャンセルする + + パノラマ 自動切り抜き + パノラマ撮影時、波打っている端の部分を切り落とします\n%s + + パノラマ撮影をする場合、端末を縦に構えて撮影ボタンを押します。そして白い円が青い点と重なるように端末を左または右に動かします。新しく撮影されるたび青い点と重なるよう端末を回転させ続けます。\n\n保存するにはチェックマーク、キャンセルするには×マークを押します。\n\n注意:パノラマ撮影は処理と保存に時間がかかります。 + + パノラマ 元画像保存 + パノラマ撮影時、元の画像を保存します。注意:パノラマ写真の保存には時間がかかります。問題を報告するのに役立つXMLファイルを保存することもできます。\n%s + + 元画像を保存しない + 元画像を保存する + 元画像とデバッグ用XMLを保存する + + ID + + 露出補正値変更 + + 写真を保存する形式を指定します。これはRAW以外の写真に適用されます。注意:ここで指定可能なPNG形式は品質設定100%%のJPEGから変換されたもので、本当の意味で可逆ではありません。\n%s + + カメラ用API + Camera2 API を使用すると、露出、フォーカス、ホワイトバランス、RAW(対応している場合)が利用可能になります。APIを変更するとアプリは再起動します。\n%s + 従来のカメラ用API + Camera2 API + + 音または音声コマンドに反応して撮影/録画を行います。 + この機能をオンにした場合、画面上に表示されるマイクボタンを使用してリスニングをスタート/ストップします。 + 注意:音声コマンドにはAndroidの音声認識サービスを使用します。このオプションを使用する場合、音声データは + 音声認識のためリモートサーバーへ送信されます。 + \n%s + + もし可能であれば、GPS座標と一緒に住所もスタンプします。 + この機能にはインターネット接続が必要です。 + 注意:住所の使用が有効になっている場合、GPS座標から住所への変換をするため端末の位置情報を含むリクエストが第三者へ送信されます。 + 詳しくはこちら https://developer.android.com/reference/android/location/Geocoder + \n%s + + フォーカスするには画面をタッチ、そして写真を撮るには青いカメラボタンを押します。 + \n\n画面の輝度が最大になるのを無効にする場合は、[設定]->[画面表示設定]->[明るさを常に最大にする]をオフにします。 + Android5以降の端末でSDカードに保存するには、[設定]->[カメラ制御の詳細設定]->[ストレージへのフレームワークを使う]を設定します。 + 詳しくは、[設定]->[オンラインヘルプ]をご覧ください。 + + + リモートデバイスの選択 + オフ + スクリーンボタンのみ隠す + GUIを隠す + すべてを隠す diff --git a/app/src/main/res/values-ko/arrays.xml b/app/src/main/res/values-ko/arrays.xml index ba079830a1aeed174d25e129b5e061420099aa2d..57ef5437c10b7fa475cbae08bceb7d12661e19cd 100644 --- a/app/src/main/res/values-ko/arrays.xml +++ b/app/src/main/res/values-ko/arrays.xml @@ -94,18 +94,6 @@ 0 180 - - 끄기 - 화면상의 가상 탐색 버튼만 숨기기 - GUI 숨기기 - 모두 숨기기 - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - 없음 3x3 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 89ce726d08b9f5cb1044279bfcaaa89bf50a0075..02826698ea904f38b913f025637e19f653c0d8a5 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -288,6 +288,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x 무제한 지연 없음 @@ -331,4 +338,8 @@ 카메라 정보 서비스 약관 + 끄기 + 화면상의 가상 탐색 버튼만 숨기기 + GUI 숨기기 + 모두 숨기기 diff --git a/app/src/main/res/values-nb/arrays.xml b/app/src/main/res/values-nb/arrays.xml index 65a26d728ecdbca004a7d5219cddf4637e5cf70c..b1d5b42f1a52d7d58e69bea513669fc5aa91f1a2 100644 --- a/app/src/main/res/values-nb/arrays.xml +++ b/app/src/main/res/values-nb/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Av - Skjul bare de virtuelle navigasjonsknappene - Skjul grensesnittet - Skjul alt - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Rød Grønn diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 060705ef49d7b5cd139af15dc9ea90e26ea0505f..cf7a2f8408cb65ecd5c4c1372bb0cf7ba381e2c8 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -112,7 +112,7 @@ Lagringsplasseringen Mappen som bildene/videoen skal lagres i. Bruk lagringstilgangsrammeverket - Hvorvidt Android 5.x sin lagringstilgangsrammeverk skal brukes til å lagre bilder og videoer. + Hvorvidt lagringstilgangsrammeverk skal brukes til å lagre bilder og videoer. Forstavelsen til fotolagring Forstavelsen som skal brukes til lagringsfilnavnene til bildene. Forstavelsen til videolagring @@ -228,7 +228,7 @@ Doner for å støtte utviklingen. Hvis du liker denne appen, så tenk gjerne på om du vil gi meg en donasjon for å støtte utviklingen. Du kan gjøre dette ved å kjøpe min "donasjonsapp" — Klikk på denne innstillingen for å gå til siden for min donasjonsapp. Takk! Bruk Camera2-APIen - Aktiverer Android 5.x sin Camera2-API - Den tilbyr ekstra egenskaper, men kan kanskje ikke virke riktig på alle enheter (Vil føre til en omstart). + Aktiverer Camera2-API - Den tilbyr ekstra egenskaper, men kan kanskje ikke virke riktig på alle enheter (Vil føre til en omstart). Om Camera App- og avlusings-informasjon Tilbakestill innstillinger @@ -494,6 +494,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Ubegrenset Ingen forsinkelse @@ -546,7 +553,6 @@ Skyggelagt tekst Ren tekst - About Bygg versjon Kamera er gaffel fra OpenCamera Forfatter @@ -555,4 +561,8 @@ Kamerainformasjon Tjenestevilkår + Av + Skjul bare de virtuelle navigasjonsknappene + Skjul grensesnittet + Skjul alt diff --git a/app/src/main/res/values-pl/arrays.xml b/app/src/main/res/values-pl/arrays.xml index 6ba9709d267644bf22f00a236fb6d9fc335cb5e3..a5faaf0494c5c9f96221f31e8547c27909358a97 100644 --- a/app/src/main/res/values-pl/arrays.xml +++ b/app/src/main/res/values-pl/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Wyłączone - Ukryj tylko przyciski nawigacyjne - Ukryj graficzny interfejs - Ukryj wszystko - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Czerwony Zielony diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c41eedb8c9eed065d8c2dbf52ac905f513724faa..b335066570fddfac00f973bc31df8a63dcd73740 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -111,7 +111,7 @@ Lokalizacja zapisu Folder, do którego będą zapisywane pliki Używaj SAF (systemowa obsługa dostępu do pamięci) - Używa Storage Access Framework Androida 5 do zapisu plików zdjęć i filmów + Używa Storage Access Framework do zapisu plików zdjęć i filmów Przedrostek nazw zdjęć Przedrostek używany w nazwach plików zdjęć Przedrostek nazw filmów @@ -227,7 +227,7 @@ Przekaż darowiznę Jeśli spodobała Ci się ta aplikacja rozważ przekazanie darowizny, która wesprze rozwój aplikacji. Możesz to zrobić poprzez zakup specjalnej aplikacji - dotknij tutaj aby otworzyć stronę. Dzięki! Używaj Camera2 API - Włącza Camera2 API Androida 5, które oferuje wiele dodatkowych funkcji, ale może działać niepoprawnie na niektórych urządzeniach (może spowodować restart) + Włącza Camera2 API, które oferuje wiele dodatkowych funkcji, ale może działać niepoprawnie na niektórych urządzeniach (może spowodować restart) O aplikacji Informacje o aplikacji i debugowanie Reset ustawień @@ -420,6 +420,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Bez limitu Bez opóźnienia @@ -481,4 +488,8 @@ Informacje o kamerze Warunki usługi + Wyłączone + Ukryj tylko przyciski nawigacyjne + Ukryj graficzny interfejs + Ukryj wszystko diff --git a/app/src/main/res/values-pt-rBR/arrays.xml b/app/src/main/res/values-pt-rBR/arrays.xml index b583d6ca3441a53521ea6877c53da7bc3183b3f8..68086d712f3b73ced7104f1457c1d4eee79b7cf9 100644 --- a/app/src/main/res/values-pt-rBR/arrays.xml +++ b/app/src/main/res/values-pt-rBR/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Desligado - Somente esconder as teclas virtuais de navegação - Esconder IGU - Esconder tudo - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Red Green diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 1e7ed03dffcf1c09e459eb4457eb973ab8dad66c..da91df347e77ec6f0e7ea71fd3afdc2438f2aff9 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -99,7 +99,7 @@ Pausar depois de tirar foto Pausar a tela depois de tirar uma foto, com a opção para compartilhar ou deletar foto Som de captura - Toca um som quando tira uma foto (requer Android 4.2+ para desabilitar) + Toca um som quando tira uma foto Teclas de volume Local de armazenamento Escolha a pasta para salvar os arquivos de foto/vídeo @@ -288,6 +288,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Ilimitado Nenhum @@ -331,4 +338,8 @@ Informações da câmera Termos de serviço + Desligado + Somente esconder as teclas virtuais de navegação + Esconder IGU + Esconder tudo diff --git a/app/src/main/res/values-pt-rPT/arrays.xml b/app/src/main/res/values-pt-rPT/arrays.xml index ff854af8f42fc014051053db36721c6adbaac8b3..e464ce02002fa3376a7e4358b3107fce07be39e1 100644 --- a/app/src/main/res/values-pt-rPT/arrays.xml +++ b/app/src/main/res/values-pt-rPT/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Desligado - Apenas ocultar os botões de navegação virtuais - Ocultar interface - Ocultar tudo - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Vermelho Verde diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 1fe459b5de3ae81c37b297f09640a79ec6d5dacc..5f0c1d14a6448d393becbed3c12e0ca9fa48c7d5 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -112,7 +112,7 @@ Local de armazenamento Pasta para guardar os ficheiros das fotografias/vídeos Usar Storage Access Framework - Optar por usar o Storage Access Framework do Android 5 para guardar fotografias e vídeos + Optar por usar o Storage Access Framework para guardar fotografias e vídeos Prefixo das fotografias guardadas O prefixo usado para guardar o nome dos ficheiros das fotografias Prefixo dos vídeos guardados @@ -228,7 +228,7 @@ Doar para apoiar o desenvolvimento Se gosta desta aplicação, por favor concidere fazer um donativo para apoiar o desenvolvimento. Pode fazê-lo ao comprar a minha "aplicação de donativo" - clique nesta opção para abrir a página da minha aplicação de donativo. Obrigado! Usar API Camera2 - Ativa a API Camera2 do Android 5 - oferece funcionalidades extra, mas é atualmente experimental (causará reinício) + Ativa a API Camera2 - oferece funcionalidades extra, mas é atualmente experimental (causará reinício) Sobre Informações da aplicação e de depuração Repor definições @@ -360,6 +360,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Ilimitado Sem atraso @@ -421,4 +428,8 @@ Informações da câmera Termos de serviço + Desligado + Apenas ocultar os botões de navegação virtuais + Ocultar interface + Ocultar tudo diff --git a/app/src/main/res/values-ru/arrays.xml b/app/src/main/res/values-ru/arrays.xml index b0bf2eb252eb056b5ca7c90d97d70a029c05c709..01d45dc4adfe0d786d2faec474d24b6a3c411d5d 100644 --- a/app/src/main/res/values-ru/arrays.xml +++ b/app/src/main/res/values-ru/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Выключить - Скрыть кнопки навигации - Скрыть интерфейс - Скрыть все - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Красный Зеленый diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ceab0e67a8a49da092ba26e1ed70c90fcf79b180..5fc91f3252cf42a8d41f7811650a67d952017964 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -91,21 +91,22 @@ Озвучивать обратный отсчет для таймера или пакетного режима (от 60 секунд) Пакетный Интервал в пакетном режиме - Дополнительные настройки + Дополнительные настройки… Касание для съемки Делать снимок по одинарному или двойному касанию области просмотра Пауза после съёмки Пауза после снятия фото, с возможностью поделиться или удалить фотографию Звук затвора - Воспроизведение звука при съемке фотографии (требуется Android 4.2+ для отключения) + Воспроизведение звука при съемке фотографии Клавиши громкости Настройки управления звуком + Чувствительность управления звуком Чувствительность уровня шума для управления звуком (опция \"громкий шум\") Папка сохранения Имя папки для сохранения фото и видео (также может быть полный путь) Использовать Storage Access Framework - Для Android 5 использовать Storage Access Framework для сохранения фото и видео + Использовать Storage Access Framework для сохранения фото и видео Префикс для фото Префикс имен файлов для сохраняемых фото Префикс для видео @@ -119,7 +120,7 @@ При записи видео графический интерфейс будет заблокирован для предотвращения случайной остановки записи. Проведите по экрану, чтобы разблокировать. Обратите внимание, что запись видео остановится, если приложение перейдет в фоновый режим или погаснет экран. Поворот предпросмотра Возможность поворота предпросмотра (не повлияет на фото/видео)\n%s - Настройки интерфейса + Настройки интерфейса… Размер предпросмотра Расположение интерфейса Режим погружения @@ -151,7 +152,7 @@ Руководство по обрезке Отображение прямоугольника, показывающего соотношение сторон - полезно, если вы планируете обрезать фото/видео. Требуется режим фотосъемки WYSIWYG или режим видео\n%s Всплывающие сообщения - Показывать всплыващие уведомления при работе + Показывать всплывающие уведомления при работе Показать эскиз анимации Отображение перемещения эскиза анимации при съемке фотографии Показывать рамку во время фотосъемки @@ -173,14 +174,14 @@ Хранить направление компаса Хранить данные о направлении компаса в фотографиях Требовать данные о местоположении - Если данные о местоположении включены, вы сможете снять фото/видео только, если они доступны + Если данные о местоположении включены, вы сможете снять фото/видео только при их доступности Штампы Сохранение вместе с фотографией таких данных как: дата/время/местоположение\n%s Формат даты Формат времени Формат GPS - Подписывание фото - Добавление текста на фотографию + Подписывать фото + Добавлять текст на фотографию Размер шрифта Размер шрифта для добавляемого на фотографию текста Цвет @@ -194,16 +195,16 @@ При выборе разрешения (3840х2160) для записи через заднюю камеру она будет работать только, если ваше устройство поддерживает данный формат! Включить стабилизацию видео Стабилизация видео уменьшает тряску в связи с движением камеры - Видео битрейт (примерно) + Битрейт видео (примерно) Выбор битрейта видео. (чем выше, тем лучше качество, но файл будет занимать больше места на диске. Неподдерживаемый битрейт может стать причиной ошибки при записи видео).\n%s Частота кадров видео (примерно) - Кол-во кадров в секунду (FPS) в видео (примерное). Может привести к записи с ошибкой, если частота кадров не поддерживается\n%s + Кол-во кадров в секунду (FPS) в видео (примерное). Может привести к записи с ошибкой, если частота кадров не поддерживается. Не гарантируется, что будет достигнуто. Пожалуйста, проверьте итоговые видео, чтобы узнать фактическую частоту кадров.\n%s Максимальная длительность видео Видеозапись прекратится по истечении указанной продолжительности\n%s - Записать новое видео после достижения лимита - При достижении лимита продолжительности, будет записано новое видео (столько раз, сколько вы выбрали)\n%s + Записывать новое видео после достижения лимита + При достижении лимита продолжительности будет записано новое видео (столько раз, сколько вы выбрали)\n%s Максимальный размер файла видео - Видеозапись прекратится или начнется заново (смотрите опцию ниже), когда размер файла достигнет указанной величины (приблизительно). Стоит заметить, что почти у всех Android-устройств есть ограничение на максимальный размер видео (обычно около 2 или 4 Гб), однако с помощью этой опции можно указать конкретное значение\n%s + Видеозапись прекратится или начнется заново (смотрите опцию ниже), когда размер файла достигнет указанной величины (приблизительно). Важно: почти у всех Android-устройств есть ограничение на максимальный размер видео (обычно около 2 или 4 Гб), однако с помощью этой опции можно указать конкретное значение\n%s Начать видеозапись заново при достижении максимального размера файла Автоматически начать видеозапись заново при достижении максимального размера файла (обусловленного ограничениями устройства или указанного пользователем) Запись звука @@ -215,12 +216,12 @@ Вспышка во время записи видео Если включено, вспышка будет включаться/выключаться при записи видео (можно использовать, чтобы выполнить запись на расстоянии) Разное - Онлайн помощь + Помощь в Интернете Запустить веб-сайт Camera в вашем браузере Пожертвовать Если вам нравится это приложение, киньте мне копейку. Вы можете сделать это, купив мое приложение. Спасибо! Использовать Camera2 API - Разрешает использование Camera2 API для Android 5 - дополнительные, но экспериментальные возможности (требуется перезапуск) + Разрешает использование Camera2 API - дополнительные, но экспериментальные возможности (требуется перезапуск) О программе Информация Сброс настроек @@ -265,9 +266,11 @@ Выключить управление звуком Скажите \"cheese\" Создайте громкий шум - Автоматическая стабилизация будет поворачивать фотографии для их выравнивания.\n\nСтоит заметить, что разрешение полученных фото слегка уменьшится из-за поворота и обрезки. - Файлы DNG содержат несжатую и необработанную информацию с вашей камеры.\n\nБольшинство программ для просмотра изображений не поддерживают DNG. Их нужно открывать специальными программами наподобие Snapseed или Lightroom.\n\nСтоит заметить, что различные опции наподобие "Штамп на фото" или "Автоматическая стабилизация" работают только для изображений формата JPEG, а не DNG.\n\nФайлы DNG имеют большой объем. Для их копирования или удаления может быть полезен файловый менеджер.\n\nФайлы DNG сохраняются только в режиме фото "Стандарт". - Режим HDR полезен для фото с большой разницей яркости. В нем делается несколько фото с различной экспозицией, которые объединяются в окончательное фото.\n\nСтоит заметить, что HDR не рекомендуется для съемки кадров с быстрым движением, что может привести к менее корректной цветопередаче.\n\nСъемка фото в режиме HDR будет гораздо медленнее, чем в обычном режиме. + + Автоматическая стабилизация будет поворачивать фотографии для их выравнивания.\n\nВажно: разрешение полученных фото слегка уменьшится из-за поворота и обрезки. + Файлы DNG содержат несжатую и необработанную информацию с вашей камеры.\n\nБольшинство программ для просмотра изображений не поддерживают DNG. Их нужно открывать специальными программами наподобие Snapseed или Lightroom.\n\nВажно: различные опции наподобие "Штамп на фото" или "Автоматическая стабилизация" работают только для изображений формата JPEG, а не DNG.\n\nФайлы DNG имеют большой объем. Для их копирования или удаления может быть полезен файловый менеджер.\n\nФайлы DNG сохраняются только в режиме фото "Стандарт". + Режим HDR полезен для фото с большой разницей яркости. В нем делается несколько фото с различной экспозицией, которые объединяются в окончательное фото.\n\nВажно: HDR не рекомендуется для съемки кадров с быстрым движением, это может привести к менее корректной цветопередаче.\n\nСъемка фото в режиме HDR будет гораздо медленнее, чем в обычном режиме. + Больше не показывать Режим фото Std @@ -276,16 +279,16 @@ HDR Expo {} Брекетинг экспозиции - []]] Серийная съемка NR Снижение шума Штамп на фото Сохранять все изображения в режиме HDR - Если эта опция включена, то в режиме HDR вместе с окончательным фото HDR будут сохранены три основных фото экспозиции. Стоит заметить, что сохранение фото будет медленнее, особенно при включенных опциях наподобие "Штамп на фото" или "Автоматическая стабилизация". + Если эта опция включена, то в режиме HDR вместе с окончательным фото HDR будут сохранены три основных фото экспозиции. Важно: сохранение фото будет медленнее, особенно при включенных опциях наподобие "Штамп на фото" или "Автоматическая стабилизация". + РАЗРЕШЕНИЯ НЕ ДОСТУПНЫ Требуется разрешение - Требуется разрешение на доступ к камере для ее сключения + Требуется разрешение на доступ к камере для ее включения Требуется разрешение на чтение/запись для сохранения фото Требуется разрешение на доступ к микрофону для записи видео со звуком Требуется разрешение на определение местоположения для геотеггинга (хранение информации о местоположении @@ -472,7 +475,7 @@ Экранная вспышка: включено Экранный фонарик Антибэндинг - Алгоритмы для борьюы с мерцанием света.\n%s + Алгоритмы для борьбы с мерцанием света.\n%s Автоматически 50 Гц 60 Гц @@ -582,8 +585,11 @@ 30x 40x 50x - Навсегда - + 100x + 200x + 500x + Навсегда + Без задержки 0.5 секунды 1 секунда @@ -632,7 +638,7 @@ -2 -3 (низкая чувствительность) Улучшение контрастности HDR - Использовать алгоритм улучшения контрастности для HDR. Может улучшить качество при очень высоком динамическом диапазоне в кадре, в результате чего изображение станосится "похожим на HDR".\n%s + Использовать алгоритм улучшения контрастности для HDR. Может улучшить качество при очень высоком динамическом диапазоне в кадре, в результате чего изображение становится "похожим на HDR".\n%s Выкл. Умный Всегда @@ -716,6 +722,7 @@ Удаленное устройство подключено Kraken Smart Housing Использовать адреса + По возможности показывать адреса вместе с GPS-координатами Предпочитать адреса GPS-координатам Не показывать адреса @@ -734,14 +741,16 @@ Показывать полосы зебры Если данная настройка включена, на экране будут появляться полосы зебры при чрезмерной экспозиции.\n%s Выкл. - 70% - - 80% - - 90% - - 100% - + 70% + 80% + 90% + 93% + 95% + 97% + 98% + 99% + 100% + Снижение фокусировки Если данная настройка включена, грани (контуры) фокуса будут освещены. В основном это применяется для ручной фокусировки, также его можно использовать для определения зон изображения находящихся в фокусе.\n%s Выкл. @@ -755,9 +764,11 @@ Показывать ярлык вспышки Показывать на экране ярлык для задания цикла вспышки вместо отображения настроек вспышки во всплывающем меню Разрешать RAW для брэкетинга экспозиции - Настройка также прездназначена для применения RAW для режима брэкетинга экспозиции фото (или для режима HDR, когда включена настройка \"Сохранять все изображения для режима HDR\"). Если данная настройка отключена, в данных режимах фото будут сохранены только в формате JPEG. + Настройка также предназначена для применения RAW для режима брэкетинга экспозиции фото (или для режима HDR, когда включена настройка \"Сохранять все изображения для режима HDR\"). Если данная настройка отключена, в данных режимах фото будут сохранены только в формате JPEG. + Разрешать RAW для фокус-брэкетинга - Настройка также прездназначена для применения RAW для режима фокус-брэкетинга фото. Если данная настройка отключена, в режиме фокус-брэкетинга фото будут сохранены только в формате JPEG. + Настройка также предназначена для применения RAW для режима фокус-брэкетинга фото. Если данная настройка отключена, в режиме фокус-брэкетинга фото будут сохранены только в формате JPEG. + Переключать режимы RAW Показывать ярлык RAW Показывать на экране ярлык для переключения режимов RAW @@ -766,27 +777,57 @@ Текст с тенью Обычный текст Текст с тенью фона - СОхранение изображений… + + Сохранение изображений… + Закончить панораму Отменить панораму Панорама отменена Не удалось создать панораму Автоматическая обрезка панорамы Удалять волнистые края в режиме панорамы\n%s - Чтобы снять панораму, удерживайте ваше устройство в портретной ориентации и нажмите на кнопку снятия фото для начала формирования панорамыa. Затем поворачивайте устройство влево или вправо, сдвигая белый круг в центре на синюю точку. После захвата нового изображения продолжайте поворачивать устройство, чтобы захватить следующую синюю точку.\n\nНажмите на галочку, чтобы сохранить панораму, или крестик, чтобы отменить ее.\n\nВажно: обработка и сохранение фото панорамы могут занять некоторое время. + + Чтобы снять панораму, удерживайте ваше устройство в портретной ориентации и нажмите на кнопку снятия фото для начала формирования панорамы. Затем поворачивайте устройство влево или вправо, сдвигая белый круг в центре на синюю точку. После захвата нового изображения продолжайте поворачивать устройство, чтобы захватить следующую синюю точку.\n\nНажмите на галочку, чтобы сохранить панораму, или крестик, чтобы отменить ее.\n\nВажно: обработка и сохранение фото панорамы могут занять некоторое время. + Оригинальные изображения панорамы Сохранять оригинальные изображения в режиме панорамы. Важно: фото панорам будут сохраняться медленнее. Также позволяет сохранять файл XML, который может быть полезен при сообщении об ошибках с панорамами.\n%s Не сохранять оригинальные изображения Сохранять оригинальные изображения Сохранять оригинальные изображения и файл XML для отладки - ID - About - Версия сборки - Камера разветвлена от OpenCamera - автор - Исходный код - лицензия - Информация о камере - Условия обслуживания - + + ID + + Шкала для изменения компенсации экспозиции + + Выберите формат файла для сохранения фото. Это влияет на \"обычные\" (не RAW) фото. Важно: формат PNG не является настоящим форматом "без потерь", вместо этого он конвертируется из JPEG со 100%% качеством.\n%s + + Camera API + Выберите Camera2 API, чтобы включить дополнительные возможности, например ручные режимы экспозиции, фокус, баланс белого, вместе с форматом RAW (если поддерживается устройством). При изменении API потребуется перезапуск.\n%s + Оригинальный API камеры + Camera2 API + + Снимать фото/видео при обнаружении шума или по голосовой команде. + Если включено, используйте экранную кнопку микрофона для запуска/остановки слушания. + Важно: настройка голосовой команды использует службу распознавания речи Android. При использовании этой опции аудиоданные + могут быть переданы на удаленные серверы для распознавания речи. + \n%s + + По возможности использовать адрес при штамповании геолокации по GPS. + Для данной опции требуется наличие Интернет-соединения. + Важно: если включено, ваше устройство будет передавать данные геолокации сторонним лицам + для преобразования координат GPS в адрес. Смотрите https://developer.android.com/reference/android/location/Geocoder . + \n%s + + Коснитесь для фокуса и нажмите синюю кнопку камеры, чтобы сделать фото. + \n\nДля отключения максимальной яркости экрана перейдите в Настройки/Настройки интерфейса…/\"Максимальная яркость\". + Для сохранения на SD-карту на Android 5+ перейдите в Настройки/Дополнительные настройки…/\"Использовать Storage Access Framework\". + Для получения подробностей нажмите \"Помощь в Интернете\" в меню Настройки. + + + Выбрать удаленное устройство + + Выключить + Скрыть кнопки навигации + Скрыть интерфейс + Скрыть все \ No newline at end of file diff --git a/app/src/main/res/values-sl/arrays.xml b/app/src/main/res/values-sl/arrays.xml index 30f1278b29fd9c7aea33c74595fb606672112df3..1a300e136f16b638875ede6b6383ea8e1c54b0f4 100644 --- a/app/src/main/res/values-sl/arrays.xml +++ b/app/src/main/res/values-sl/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Izk. - Skrij samo zaslonske gumbe za navidezno krmarjenje - Skrij gr. up. vmsnik - Skrij vse - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Rdeča Zelena diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 50556f0ae8cb7dc99cda6e72a5915f8b4354bc3b..e6d1b313de66dc0dab38098a2978548765c7d511 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -112,7 +112,7 @@ Mesto za shranjevanje Mapa za shranjevanje datotek fotografij/videoposnetkov Uporabi ogrodje za dostop do pomnilniške naprave - Ali naj se uporabi ogrodje za dostop do pomnilniške naprave Androida 5 za shranjevanje fotografij in videoposnetkov + Ali naj se uporabi ogrodje za dostop do pomnilniške naprave za shranjevanje fotografij in videoposnetkov Predpona shranjenih fotografij Predpona imena datotek fotografij Predpona shranjenih videoposnetkov @@ -228,7 +228,7 @@ Daruj za podporo razvoju Če vam je ta program všeč, razmislite o darovanju, da podprete razvoj. To lahko naredite z nakupom mojega "programa za darovanje" - kliknite na to možnost za odpiranje strani z mojim programom. Hvala! Uporabi API Camera2 - Omogoči API Camera2 Androida 5 - ponuja dodatne značilnosti, vendar morda ne bo pravilno deloval na vseh napravah (lahko povzroči ponovni zagon) + Omogoči API Camera2 - ponuja dodatne značilnosti, vendar morda ne bo pravilno deloval na vseh napravah (lahko povzroči ponovni zagon) O programu Program in podatki za razhroščevanje Ponastavi nastavitve @@ -499,6 +499,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Neomejeno Brez zakasnitve @@ -560,4 +567,8 @@ Podatki o fotoaparatu Pogoji storitve + Izk. + Skrij samo zaslonske gumbe za navidezno krmarjenje + Skrij gr. up. vmsnik + Skrij vse diff --git a/app/src/main/res/values-tr/arrays.xml b/app/src/main/res/values-tr/arrays.xml index d31fec9ef22a79b6aae8eb469865452b231a310e..b0d049e35f49188a60f387dc3a97dafcc5d25076 100644 --- a/app/src/main/res/values-tr/arrays.xml +++ b/app/src/main/res/values-tr/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Kapalı - Sadece sanal ekran navigasyon butonlarını gizle - GUI(Grafik arayüz) gizle - Herşeyi gizle - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Kırmızı Yeşil diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3d3242a3704f9f8d8615cd9f1de4a1ae8a6f4bfa..01cedf0333ac0079f62c26deb109b4e7c5c5da3e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -103,13 +103,13 @@ Fotoğraf çektikten sonra beklet Fotoğraf çektikten sonra fotoğrafı paylaşma ya da silme seçeneği için ekranı duraklat Fotoğraf çekme sesi - Fotoğraf çekerken ses çal(kapatmak için Android 4.2 ya da daha üstü gereklidir) + Fotoğraf çekerken ses çal Ses seviyesi tuşları Kaydetme yeri Fotoğraf/vidyo dosyaları için kayıt klasörü Storage Access Framework kullan - Android 5\'s fotoğraf ve vidyoları kaydetmek için Storage Access Framework kullan + Fotoğraf ve vidyoları kaydetmek için Storage Access Framework kullan Fotoğraf adlandırma öneki kaydet Fotoğraflarda dosya adlarını kaydetmede kullanılacak önek vidyo adlandırma öneki kaydet @@ -213,7 +213,7 @@ Geliştirmeyi desteklemek için bağışta bulunun Uygulamayı beğendiyseniz, bağış yapmayı düşünebilirsiniz. "donation app" satın alarak bunu yapabilirsiniz - bu seçeneğe tıkladığınızda bağış uygulaması için sayfam açılacaktır. Teşekkürler! Camera2 API sini kullan - Etkinleştirir Android 5\'s Camera2 API - ekstra özellikler sunar fakat şuan deneyseldir(yeniden başlatacaktır) + Etkinleştirir Camera2 API - ekstra özellikler sunar fakat şuan deneyseldir(yeniden başlatacaktır) Hakkında Uygulama ve eksiklik bilgisi Ayarları temizle @@ -322,6 +322,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Sınırsız Gecikmesiz @@ -368,4 +375,8 @@ Kamera bilgileri Hizmet şartları + Kapalı + Sadece sanal ekran navigasyon butonlarını gizle + GUI(Grafik arayüz) gizle + Herşeyi gizle diff --git a/app/src/main/res/values-uk/arrays.xml b/app/src/main/res/values-uk/arrays.xml index a5a9c9cfe9cecacea947f504c1a7d99aba5095b3..fc4ac72938d95b9a3aa1bf23e68698405a4615bd 100644 --- a/app/src/main/res/values-uk/arrays.xml +++ b/app/src/main/res/values-uk/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Вимкнути - Приховати кнопки навігації - Приховати GUI - Приховати все - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Червоний Зелений diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1c3c8ca2dc16092a2022297eafbf615524a4a200..17da6df09db2eb74462db544bdd778b527f8e07f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -104,7 +104,7 @@ Пауза після знімку Пауза після зняття фото, з можливістю поділитися чи видалити фотографію Звук затвору - Відтворення звуку під час знімку фотографії (потрібно версія Android 4.2+ аби вимкнути) + Відтворення звуку під час знімку фотографії Кнопки гучності Налаштування керування звуком @@ -113,7 +113,7 @@ Тека збереження "Ім'я теки для збереження фото та відео (також може бути повний шлях)" Використовувати Storage Access Framework - Для Android 5 використовувати Storage Access Framework для збереження фото та відео + Використовувати Storage Access Framework для збереження фото та відео Префікс для фото Префікс імен файлів для знімків Префікс для відео @@ -229,7 +229,7 @@ Подякувати Якщо вам сподобався цей додаток, киньте монетку. Ви можете зробити це, придбавши мій додаток. Дякую! Використовувати Camera2 API - Дозволяє використання Camera2 API для Android 5 - додаткові, але експериментальні можливості (потрібно перезавантажити) + Дозволяє використання Camera2 API - додаткові, але експериментальні можливості (потрібно перезавантажити) Про додаток Інформація Скидання налаштувань @@ -445,6 +445,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x Постійно Без затримки @@ -506,4 +513,8 @@ Інформація про камеру Умови обслуговування + Вимкнути + Приховати кнопки навігації + Приховати GUI + Приховати все diff --git a/app/src/main/res/values-vi/arrays.xml b/app/src/main/res/values-vi/arrays.xml index dea3945590d7affc27801ad96584b76b004a15dc..c23edde52ef32de74e3797d28230ad53a6320af0 100644 --- a/app/src/main/res/values-vi/arrays.xml +++ b/app/src/main/res/values-vi/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - Tắt - Chỉ ẩn các nút điều hướng - Ẩn giao diện người dùng - Ẩn mọi thứ - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Đỏ Lục diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f7a7348ba770eaea8dab55f0710f77cea7679963..f338ee4c617c816958654f30ed540c2601d4c6bb 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -112,7 +112,7 @@ Vị trí lưu Thư mục lưu tệp tin ảnh/video Sử dụng Khung Truy cập Lưu trữ - Sử dụng Khung Truy cập Lưu trữ để lưu ảnh và video. Nên kích hoạt (từ Android 5 trở lên) để có thể lưu vào thẻ nhớ ngoài. + Sử dụng Khung Truy cập Lưu trữ để lưu ảnh và video. Nên kích hoạt để có thể lưu vào thẻ nhớ ngoài. Tiền tố ảnh Đặt tiền tố trong tên tệp khi lưu ảnh Tiền tố video @@ -292,7 +292,7 @@ HDR EXPO Phơi sáng mở rộng - FB + Chụp nhanh NR Giảm nhiễu @@ -645,6 +645,9 @@ 30x 40x 50x + 100x + 200x + 500x Không giới hạn Không @@ -788,4 +791,8 @@ Thông tin camera Điều khoản dịch vụ + Tắt + Chỉ ẩn các nút điều hướng + Ẩn giao diện người dùng + Ẩn mọi thứ diff --git a/app/src/main/res/values-zh-rCN/arrays.xml b/app/src/main/res/values-zh-rCN/arrays.xml index 6365bd34c4561a494ae9de1b77c1b0cbca9b56db..dae98a91f339a5c0fb708ac804e37944e0df5b9c 100644 --- a/app/src/main/res/values-zh-rCN/arrays.xml +++ b/app/src/main/res/values-zh-rCN/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - 关闭 - 只隐藏屏幕上的虚拟导航按钮 - 隐藏图形用户界面 - 隐藏一切 - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - 红色 绿色 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a3d8ab7ea2ac3a3fb9bae5841ddb85cddb9e7b70..e0b043c7e7cb9e6f8e4ddb6b702562542891d2f0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -103,7 +103,7 @@ 拍照后暂停 拍照后暂停屏幕,带有选项分享或删除照片 快门声音 - 拍照时播放声音(需要 Android 4.2 或更高才能禁用) + 拍照时播放声音 音量键功能 语音识别 麦克风灵敏度 @@ -111,7 +111,7 @@ 保存位置 用来保存照片/视频文件的文件夹 使用存储访问架构 - 是否使用 Android 5 的存储访问架构保存照片和视频 + 是否使用存储访问架构保存照片和视频 保存照片前缀 用于保存照片文件名的前缀 保存视频前缀 @@ -220,7 +220,7 @@ 捐赠以支持开发 如果您喜欢这个程序,可以通过购买我的“捐助 app”向我捐助。点击此选项将打开我的“捐助 app”页面。谢谢! 使用 Camera2 API - 启用 Android 5 的 Camera2 API - 提供附加功能,但当前处于实验阶段 (会引起重启) + 启用 Camera2 API - 提供附加功能,但当前处于实验阶段 (会引起重启) 关于 应用程序和调试信息 复位设置 @@ -554,6 +554,13 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x 无限 无延迟 @@ -614,7 +621,7 @@ HDR 包围曝光 包围曝光(Expo) - []]] + 快速连拍 NR 降噪(NR) @@ -774,6 +781,11 @@ 70% 80% 90% + 93% + 95% + 97% + 98% + 99% 100% 对焦峰值 @@ -834,5 +846,8 @@ 相机信息 服务条款 - + 关闭 + 只隐藏屏幕上的虚拟导航按钮 + 隐藏图形用户界面 + 隐藏一切 diff --git a/app/src/main/res/values-zh-rTW/arrays.xml b/app/src/main/res/values-zh-rTW/arrays.xml index ac2b64d265d5a32769abb103ef624fbec965914a..42fb1da33d2e6f64e6ab63dd84f4ee381d86254d 100644 --- a/app/src/main/res/values-zh-rTW/arrays.xml +++ b/app/src/main/res/values-zh-rTW/arrays.xml @@ -104,18 +104,6 @@ 0 180 - - 關閉 - 只隱藏螢幕上的虛擬按鈕 - 隱藏圖形化使用者介面 - 隱藏全部 - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index abb0432d00738de4f4dece5ea2b75386283809a9..0d6bf53dea66cc34cb960ba49fb8ed52bd17d8ee 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,22 +1,22 @@ - Open Camera - 設定 + Camera + 設定 彈出設定 好 (此訊息將不再顯示) - 開啟 - 關閉 + 開啟 + 關閉 - 選擇儲存位置: + 選擇儲存位置: 清除資料夾歷史 清除資料夾歷史? 選擇另一個資料夾 已更改儲存位置到: 已停止錄製影片 - repeats to go - 連結到相機失敗 + 次剩餘重複次數 + 重新連結到相機失敗 錯誤,影片檔可能已經損壞 在您的裝置上不支援 未知錯誤,影片中斷 @@ -41,7 +41,8 @@ 已開始錄製影片 抱歉 儲存照片失敗 - 防手震失敗 + 儲存 RAW 照片失敗 + 自動水平失敗 啟動相機預覽失敗 拍照 拍照失敗 @@ -50,7 +51,7 @@ 沒有可用的相簿程式 螢幕被鎖定\n滑動螢幕來解鎖 已解鎖 - 對不起,防手震在此裝置上\n不支援 + 對不起,自動水平在此裝置上\n不支援 停用音訊 最大持續時間 顏色效果 @@ -62,19 +63,20 @@ 打開相機失敗。 相機可能正被 其它程式使用? + ISO 縮放 可用記憶體 - [鎖定: + [已鎖定: 滑動以解鎖] 在照片上標記失敗 - m + 公尺 相機特效 - 防手震 - 圖像將被旋轉,使它們自動對準(僅照片) (拍照更慢,且可能在記憶體過低的裝置上失敗) + 自動水平 + 圖片將被旋轉,使它們自動對準(僅照片) (拍照更慢,且可能在記憶體過低的裝置上失敗) 套用一個顏色效果 - 套用選定的顏色效果到照片 + 套用選取的顏色效果到照片 套用一個場景模式 為各種場景優化照片 設定白平衡 @@ -82,11 +84,11 @@ 設定 ISO 越高的值意味著對光越敏感(在某些裝置上可能無法作用) 設定曝光補償 - 設為 0 使用預設曝光補償。 + 設為 0 使用預設曝光補償 鎖定照片/影片方向 - 如果設定,裝置的方向將被忽略,且照片/影片的方向將相對於指定的方向。\n%s + 如果設定,裝置的方向將被忽略,且照片/影片的方向將相對於指定的方向\n%s 臉部偵測 - 用臉部偵測代替對焦區。 + 用臉部偵測代替對焦區 相機控制 定時器 @@ -94,31 +96,38 @@ 當定時器倒數或連拍模式延遲時提示 語音倒數計時 定時或連拍模式時使用語音倒數計時 (從 60 秒開始) - 連拍模式 + 連拍模式 連拍模式間隔 更多相機控制… - 快門設定 + 點擊拍照 在預覽畫面按一次或兩次來拍照 拍照後暫停 拍照後暫停螢幕,顯示分享或刪除照片選項 快門聲音 - 拍照時播放聲音(需要 Android 4.2 或更高才能停用) + 拍照時播放聲音 音量鍵 + 音訊控制選項 + + 音訊控制敏感度 + 音訊(大的聲音)選項的聲音等級敏感度 儲存位置 用來儲存照片/影片檔的資料夾 - 使用 Storage Access Framework - 是否使用 Android 5 的 Storage Access Framework 來儲存照片和影片 + 使用 Storage Access Framework + 是否使用 Storage Access Framework 來儲存照片和影片。這個選項應該被啟用來允許儲存到外部 SD 卡。 照片檔名前綴 用於儲存照片檔名的前置字串 影片檔名前綴 用於儲存影片檔名的前置字串 + 檔案名稱的時間格式 鎖定時顯示相機 如果開啟,Open Camera 將在任何鎖定畫面上顯示 (您仍然必須解鎖來存取設定、相簿等) + 啟動時自動對焦 + Open Camera 啟動時是否要自動對焦。如果您有啟動時閃光燈開啟的問題,請停用此選項 錄影時鎖定螢幕 - 當錄製影片時,圖形化使用者介面將被鎖定以防止意外的停止錄影。滑動螢幕以解鎖。注意,如果程式轉到背景或螢幕是空白,影片錄製會停止。 + 當錄製影片時,圖形化使用者介面將被鎖定以防止意外的停止錄影。滑動螢幕以解鎖。注意,如果程式進入背景或螢幕關閉,影片錄製會停止。 旋轉預覽 旋轉預覽選項 (不會影響最終的照片/影片)\n%s - 螢幕GUI… + 螢幕 GUI… 預覽大小 UI 佈局 全螢幕模式 @@ -126,17 +135,17 @@ 在螢幕上顯示目前的相機縮放等級 (當放大時) 顯示縮放 -/+ 控制 顯示縮放控制的 -/+ 按鈕 - 顯示縮放滑條控制 - 顯示縮放控制的滑條 - 顯示 ISO - 顯示目前的 ISO 等級 (需要 Camera2 API) + 顯示縮放滑桿控制 + 顯示縮放控制的滑桿 + 顯示 ISO + 顯示目前的 ISO 等級。在自動閃光模式,閃光符號也會指示閃光燈何時閃光。(需要 Camera2 API。) 顯示可用記憶體 在螢幕上顯示剩餘的裝置儲存空間 顯示角度 在螢幕上顯示目前的裝置角度 顯示角度線 顯示水平線 - 角度標示顏色 + 角度標示顏色 相機接近水平時的標示顏色 顯示羅盤方向 在螢幕上顯示裝置的羅盤方向 @@ -145,15 +154,17 @@ 顯示電池 在螢幕上顯示目前電池電量 顯示格線 - + 格線 顯示裁切輔助 - 裁切輔助顯示一個矩形來表示指定的長寬比看起來如何 - 如果您計畫以後裁切照片/影片為不同的解析度很有用。需要所見即所得拍照模式,或處於錄影模式中。\n%s + 裁切輔助顯示一個矩形來表示指定的長寬比看起來如何 - 如果您計畫以後裁切照片/影片為不同的長寬比很有用。需要所見即所得拍照模式,或處於錄影模式中。\n%s 顯示拍照前提示訊息 是否要在拍照快門前顯示彈出訊息提示 顯示縮圖動畫 當拍照時顯示移動縮圖動畫 + 拍照時顯示邊緣 + 在螢幕上顯示邊緣來表示拍照 保持螢幕常亮 - 如果開啟,當 Open Camera 的主介面啟動時,螢幕將不會關閉。 + 如果開啟,當 Open Camera 的使用者介面啟動時,螢幕將不會關閉。 強制最大亮度 強制螢幕以最大亮度顯示,而不是裝置預設值 @@ -162,16 +173,17 @@ 影片設定… 位置設定… 相機解析度 - 圖像品質 - 設定儲存的照片圖像的圖像品質(建議值是90%%)\n%s + 圖片品質 + 設定儲存的 JPEG 或 WebP 照片的圖片品質(預設值是 90%%)。對於 PNG 圖片格式沒有作用。\n%s + RAW 儲存位置資料 (地理標記) - 在照片/影片中儲存 GPS 位置資料 + 在照片/影片中儲存 GPS 位置資料(對於照片,位置資料只能儲存在 JPEG 和 DNG 格式) 儲存羅盤方向 - 在照片中儲存 GPS 羅盤方向 + 在照片中儲存 GPS 羅盤方向(只有 JPEG 格式) 需要位置資料 如果啟用位置資料,只拍照/錄影如果位置資料可用。 標記照片 - 用日期和時間標記照片;如果啟用了位置/方向資料也標記 GPS 資訊\n%s + 用日期和時間標記照片;如果啟用了位置/方向資料,也標記 GPS 資訊\n%s 日期標記格式 時間標記格式 GPS 標記格式 @@ -181,40 +193,47 @@ 當在照片上標記文字時使用的字型大小 字型顏色 當在照片上標記文字時使用的字型顏色 - 文字風格 - 當在照片上標記文字時使用的字型風格\n%s + 文字樣式 + 當在照片上標記文字時使用的樣式\n%s + 使用背景執行緒 + 是否要在背景執行緒儲存照片(操作較快) 影片解析度 - 強制 4K UHD 影片 (實驗中) + 強制 4K UHD 影片(只能在某些裝置上運作) 為後置相機錄影啟用 3840x2160 解析度 - 僅當您的裝置支援時才能作用,且如果不支援可能當機! - 啟用影片穩定 - 影片穩定減少在預覽和錄影中由於相機移動引起的晃動。 + 啟用影片防震 + 影片防震減少在預覽和錄影中由於相機移動引起的晃動。 影片位元率 (大約) 設定影片的大致位元率 (越高表示越好的品質,但佔用更多磁碟空間;如果位元率不支援可能造成錄影失敗)\n%s - 影片FPS (大約) - 設定影片的 FPS(大約值,不能保證有用,且如果不支援可能造成錄影失敗)\n%s + 影片影格率 (大約) + 設定影片的影格率 (FPS)(大約值,不能保證有用,且如果不支援可能造成錄影失敗)。注意這個設定在慢動作影片會被忽略。\n%s 影片最長持續時間 - 在指定的時間後錄影將停止。\n%s + 在指定的時間後錄影將停止\n%s 在最大持續時間後重啟錄影 - 如果在達到最大持續時間(如果有設定)後影片停止,此選項將使錄影重啟,最多為指定的次數。\n%s + 如果在達到最大持續時間(如果有設定)後影片停止,此選項將使錄影重新開始,最多為指定的次數\n%s + 最大影片檔案大小 + 當到達(大約)最大檔案大小,錄影會停止和/或重新開始(見下面選項)。注意許多 Android 裝置對影片有最大檔案大小(通常約為 2GB 或 4GB),但是這個選項允許設定特定值。注意這個選項無法用於增加裝置內建的最大大小。\n%s + 到達最大檔案大小時重新開始 + 當到達最大檔案大小時,是否要自動重新開始(無論是裝置預設最大檔案大小或使用者指定) 錄音 錄影時錄音 音源 - 麥克風用於錄製音訊\n%s - 音源頻道 + 用於錄製音訊的麥克風\n%s + 音源頻道 指定使用單聲道或立體聲來錄音 (立體聲只支援某些裝置) 錄影時閃光 - 如果啟用,錄影時閃光將開啟/關閉 (在一段距離外可以用來判斷相機仍然在錄製)。 + 如果啟用,錄影時閃光將開啟/關閉 (在一段距離外可以用來判斷相機仍然在錄製) 其他 線上說明 + 在瀏覽器中開啟 Open Camera 網站 捐贈以支援開發 如果您喜歡這個程式,請考慮捐助。您可以透過購買我的“捐助 app”來進行捐助 - 點擊此選項將打開我的捐助 app 頁面。謝謝! 使用 Camera2 API - 啟用 Android 5 的 Camera2 API - 提供更多功能,但仍在實驗中 (會造成重新啟動) + 啟用額外的功能,例如手動曝光模式、對焦、白平衡和 RAW(如果裝置支援),但是無法在所有裝置上正確運作 (會造成重新啟動) 關於 應用程式和除錯資訊 - 復原設定 - 重設所有 Open Camera 設定為預設 + 重設設定 + 重設所有 Open Camera 設定為預設值 您確定要重設所有 Open Camera 設定為預設值嗎? 可用 @@ -227,8 +246,12 @@ 曝光鎖定 對焦模式 閃光模式 + 切換到前置相機 + 切換到後置相機 + 切換到影片模式 + 切換到照片模式 刪除上一張照片 - 共用照片 + 分享照片 上層資料夾 新資料夾 @@ -240,6 +263,107 @@ 選擇儲存位置 Storage Access Framework 已取消 + 無法儲存到這個資料夾 + + + 麥克風權限不可用 + 位置權限不可用 + + 開始錄製影片 + 停止錄製影片 + + 最大檔案大小 + MB + GB + + 開始音訊監聽 + 停止音訊監聽 + 說「cheese」 + 發出大的聲音 + + 自動水平會自動旋轉照片,使它們顯得水平。\n\n注意這意味著圖片解析度會較低(因為需要旋轉和裁切)。 + DNG 檔案包含來自相機的全部未壓縮和未處理的資訊。\n\n注意有些圖庫無法識別 DNG 檔案。DNG 圖片可以使用專用編輯器(例如 Snapseed 和 Lightroom)處理。\n\n注意「標記照片」和「自動水平」等處理選項僅適用於非-DNG (JPEG/etc) 圖片 ,不適用於 DNG 圖片。 + HDR 模式在亮度變化範圍大的環境中非常有用。它的工作原理是在不同曝光下拍攝多張照片,並將它們組合在一起以建立最終圖片。\n\n注意 HDR 不適合用於快速移動的場景,而且可能造成顏色較不精確。\n\nHDR 模式拍照比較慢。 + + 不要再次顯示 + + 照片模式 + 標準 + 標準 + HDR + Expo {} + 包圍曝光 + 快速連拍 + NR + 雜訊抑制 + 照片標記 + HDR 模式儲存所有圖片 + 如果啟用,使用 HDR 模式拍照會儲存三個基本曝光圖片以及最終的 HDR 照片。注意這會使儲存速度變慢,尤其是啟用了「照片標記」或自動水平等選項時。 + + 權限不可用 + 需要權限 + 需要相機權限來啟用相機 + 需要讀/寫儲存空間權限來儲存照片 + 錄製具有音訊的影片和語音控制需要麥克風權限 + 地理標記需要位置權限(在照片和影片中儲存位置資訊)。 + 連線到藍牙低功耗遠端控制裝置也需要位置權限。 + + 除錯選項 + 除錯選項 + 使用替代閃光燈方法 + 如果您的裝置使用 Camera2 API 時閃光燈有異常,請啟用此功能 + + 包圍曝光 + 包圍曝光的圖片數\n%s + 包圍曝光光圈 (Stops) + 最暗/最亮圖片要減少/增加多少光圈 (Stops)\n%s + + 雜訊抑制原圖 + 是否在雜訊抑制模式中儲存原圖。注意這會讓儲存照片慢很多。\n%s + + 不儲存原圖 + 儲存一張原圖 + 儲存所有原圖(慢) + + + + 沒有足夠的儲存空間錄製影片了 + 停止錄影\n電量嚴重不足 + 電量不足檢查 + 如果電池電量嚴重不足,則停止錄影。這會降低由於電力不足突然關機造成影片損壞的風險。 + + + + 快門速度 + + 相機發生了嚴重錯誤 + + 啟用快速 HDR/expo 連拍 + 允許更快地完成 HDR/expo 拍照。如果您的裝置在使用 HDR/expo 模式拍照時出現了問題,請停用此功能。 + + 相機 + 錄影 + 自拍 + + 前置相機鏡像 + 是否在使用前置相機時反轉左右方向\n%s + + 校正水平角度 + 此選項可以校正裝置的加速計,使自動水平和螢幕顯示的水平/角度正常運作 + 請將您的裝置放在平坦的水平面上(直向或橫向),然後按下「校正」\n\n按下「重置」可以從裝置中刪除校正資料。\n\n按返回按鈕以取消。 + 校正 + 重置 + 水平校正 + 重置水平校正 + + DRO + + 顯示傾斜線 + 顯示水平傾斜線 + 顯示羅盤方向線 + 顯示羅盤方向線 + 影片字幕 + 建立儲存日期和時間的字幕檔案 (.SRT)。如果啟用了位置/方向資料,也會記錄 GPS 資訊\n%s 無限 3 秒 @@ -265,24 +389,171 @@ 30 分 45 分 1 小時 + + 顯示拍照按鈕 + 顯示拍照、錄影按鈕。如果您要使用其他方法拍照(例如硬體快門、使用音量鍵),請取消勾選。 + + 已暫停錄影 + 已繼續錄影 + 暫停錄影 + 繼續錄影 + + 錄影中… + + 初始化音訊監聽失敗 + HDR 照片建立失敗 + + 自動 + 多雲 + 日光 + 螢光燈 + 白熾燈 + 陰翳 + 星光 + 溫暖 + 手動 + + 動作 + 條碼 + 沙灘 + 燭光 + 自動 + 煙火 + 橫向 + 夜晚 + 夜間人像 + 聚會 + 人像 + 雪景 + 運動 + 穩定拍照 + 日落 + 劇院 + + 湖藍 + 黑板 + 黑白 + 負片 + + 色調分離 + 棕褐色 + 過曝 + 白板 + + 新功能: + 捐贈 + + + + + 在螢幕左側 + 在螢幕右側 + 在螢幕頂端 + 在螢幕底端 + 在中央 + + 高速 + 預設 + 自訂 EXIF 標籤 + 藝術家 + 儲存在圖片詮釋資料藝術家標籤的文字(只有 JPEG 格式)。(留空代表不儲存) + 版權 + 儲存在圖片詮釋資料版權標籤的文字(只有 JPEG 格式)。(留空代表不儲存) + + 處理中… + + + 標準和 DNG(RAW) + 僅 DNG(RAW) + 預設 - 年-月-日 + 年-月-日 (ISO 8601) 日/月/年 月/日/年 + 照片數量 + 2 + 3 + 4 + 5 + 6 + 8 + 10 + 12 + 15 + 20 + + 照片數量 + 2 + 3 + 4 + 5 + 6 + 8 + 10 + 12 + 15 + 20 + 25 + 30 + 40 + 50 + 100 + 150 + 200 + + 速度 + 正常 + 啟用慢動作 + 關閉慢動作 + 關閉閃光 自動閃光 開啟閃光 手電筒 紅眼 + 自動螢幕閃光 + 開啟螢幕閃光 + 螢幕手電筒 + + 去除條紋 + 消除燈光閃爍造成的條紋使用的演算法。\n%s + 自動 + 50Hz + 60Hz + 關閉 + + 輔助圖片 + 在取景框內顯示一張半透明的圖片,輔助你對正鏡頭。\n%s + 關閉 + 上次拍攝的照片 + 選擇一張圖片 + + 在裝置上找不到檔案選擇器,所以不能使用輔助圖片功能 + 無法開啟圖片 + + 允許在錄影時拍照 + 允許在錄影時拍照。如果您啟用了 Camera2 API 後發現錄製影片有問題,請停用此功能。 + + 影片 flat (log) profile + 影片模式啟用 flat (log) profile\n%s + 關閉 + 細微 + + + + 超強 + Log profile 攝影機 外部麥克風(如果存在) 預設的音源 優化語音 + 針對語音辨識最佳化 + 未處理 預設 100kbps @@ -308,6 +579,63 @@ 80Mbps 90Mbps 100Mbps + 150Mbps + 200Mbps + + 沒有連拍 + + 顯示音量計 + 錄影時是否在螢幕上顯示音量等級 + + 對焦 {} + 包圍對焦 + + 包圍對焦來源距離 + 包圍對焦目標距離 + + 加入無限遠距離 + + 羅盤方向 + 您裝置的羅盤需要校正以提高準確度。這可以通過以8的形狀的動作移動手機來完成。\n\n目前準確度: + + 不可靠 + + + + 未知 + + 距離單位 + 在照片標記和影片字幕中使用的 GPS 高度的單位\n%s + 公尺 + 英尺 + 英尺 + + 顯示新功能對話框 + 是否在應用程式更新後顯示新功能和改進資訊 + + 邊緣銳化演算法 + 相機驅動程序套用邊緣銳化應該使用的演算法。邊緣銳化可以提高拍攝圖片的銳度和細節。(在 NR 拍照模式下這個設定會被忽略。)\n%s + + 處理設定… + + 雜訊抑制演算法 + 相機驅動程式套用雜訊抑制應該使用的演算法。雜訊抑制演算法會試圖去除相機拍攝的噪點(特別是在黑暗環境),改善圖片品質。(注意這與雜訊抑制照片模式無關,實際上在 NR 照片模式下此設定會被忽略。)\n%s + 預設 + 關閉 + 最小 + 快速 + 高品質 + + 輔助對焦 + 調整手動對焦距離時是否放大顯示\n%s + 關閉 + 2倍 + 4倍 + + 增加或減少曝光補償 + + 上一步 + 下一步 關閉 2x @@ -315,9 +643,17 @@ 4x 5x 10x + 20x + 30x + 40x + 50x + 100x + 200x + 500x 無限 無延遲 + 0.5秒 1秒 2秒 3秒 @@ -335,6 +671,18 @@ 1小時 2小時 + 裝置預設值 + 100MB + 200MB + 300MB + 500MB + 1GB + 2GB + 5GB + 9GB + + 取消包圍對焦 + 1 (1:1) 1.25 (5:4) @@ -343,19 +691,228 @@ 1.5 (3:2) 1.78 (16:9) 1.85 (37:20) + 2 (2:1) 2.33 (21:9) 2.35 (47:20) 2.4 (12:5) + +3 (高靈敏度) + +2 + +1 + 0 (預設) + -1 + -2 + -3 (低靈敏度) + + HDR 對比度增強 + HDR 何時使用對比度增強演算法。在動態範圍非常大的場景中能夠提高成像品質。這也會讓照片多出一種 HDR 的味道。\n%s + 關閉 + 自動 + 總是 + + 影片格式 + 影片和音訊檔案格式和編解碼器\n%s + + 預設 + MPEG4 H264 + MPEG4 HEVC + 3GPP + WebM(不支援音訊) + + 設定管理員 + + 儲存設定 + 將所有 Open Camera 的設定儲存在一個檔案中 + 儲存的檔案名稱 + + 還原設定 + 還原之前儲存的設定。注意這會覆寫現在的所有設定值! + 這個選項會讓你選擇一個之前儲存的設定檔案。需要注意選擇載入檔案後會覆寫現在所有的設定! + + 儲存的設定 + 儲存設定失敗 + 還原設定失敗 + 這個裝置沒有可用的檔案對話框,不支援這個選項 + + 圖片格式 + JPEG + WebP + PNG + + 雜訊抑制模式 + 正常 + 低照度 + 左手使用者介面 右手使用者介面 - About - 内部版本 - 相机是从OpenCamera分叉的 - 作者 - 源代码 - 牌照 - 相机信息 - 服务条款 + 圖示在頂部(直向) + + 顯示臉部偵測圖示 + 是否在螢幕上顯示圖示來啟用或停用臉部偵測 + 啟用臉部偵測 + 停用臉部偵測 + 臉部偵測已啟用 + 臉部偵測已停用 + + 顯示自動水平圖示 + 是否在螢幕上顯示圖示來啟用或停用自動水平。當自動水平啟用時,照片會被旋轉,對齊地平線。 + 啟用自動水平 + 停用自動水平 + + 顯示標記照片圖示 + 是否在螢幕上顯示圖示來啟用或停用照片標記 + 啟用照片標記 + 停用照片標記 + 已啟用照片標記 + 已停用照片標記 + + 顯示自訂文字照片標記圖示 + 是否在螢幕上顯示圖示來指定照片標示的自訂文字 + + 顯示自動白平衡鎖定圖示 + 是否在螢幕上顯示圖示來啟用或停用自動白平衡 + 鎖定白平衡 + 解鎖白平衡 + 白平衡已鎖定 + 白平衡已解鎖 + + 顯示自動曝光鎖定圖示 + 是否在螢幕上顯示圖示來鎖定或解鎖曝光 + 解鎖曝光 + + 低照度模式:請保持相機穩定 + + 全景 + 全景 + + 藍牙低功耗遠端控制… + 啟用藍牙低功耗遠端控制 + 啟用藍牙低功耗 (BLE) 遠端控制 + 遠端裝置類型 + + 不支援 BLE + 掃描 BLE 裝置 + 不支援藍牙。 + 未知裝置 + 遠端中斷連線時螢幕變暗 + 提示:啟動 Open Camera 前,請把預設亮度調到最低。 + 使用鹽水計算深度 + 使用水下防水盒時,如果選擇正確的水的類型,可以提高拍攝的準確性。 + 遠端已連線 + + Kraken Smart Housing + + 使用地址 + + 可以的話,除了 GPS 座標之外也顯示地址 + 地址優先於 GPS 座標 + 不顯示地址 + + 顯示儲存位置資訊圖示 + 是否在螢幕上顯示圖示來啟用或停用位置資訊(地理標記) + 儲存位置資訊 + 停止儲存位置資訊 + + 顯示色階分佈圖 + 是否要顯示色彩的色階分佈圖\n%s + 關閉 + RGB 色彩 + 亮度 (Luminance) + 明度 (Value)(最大) + 強度 (Intensity)(平均) + 亮度 (Lightness)(最小-最大的平均) + + 顯示斑馬條紋 + 如果啟用此選項,螢幕上會顯示斑馬條紋,顯示哪裡相機預覽曝光過度。\n%s + 關閉 + 70% + 80% + 90% + 100% + + 峰值對焦 + 如果啟用此選項,會標明對焦的邊緣(輪廓)。這主要用於手動對焦,可用於幫助確定圖片的哪些區域處於焦點。\n%s + 關閉 + 開啟 + 峰值對焦顏色 + 標明對焦的邊緣要使用的顏色\n%s + + 剩餘 + + 相機預覽… + + 循環切換閃光燈 + 顯示閃光燈圖示 + 是否在螢幕上顯示圖示來循環切換閃光燈選項,而不是在彈出式選單顯示閃光燈選項 + + 允許 RAW 包圍曝光 + RAW 選項是否也要適用於包圍曝光照片模式(或啟用了「HDR 模式儲存所有圖片」的 HDR 模式)。如果停用此選項,在那些照片模式中只會儲存 JPEG。 + + 允許 RAW 包圍對焦 + RAW 選項是否也要適用於包圍對焦照片模式。如果停用此選項,在包圍對焦照片模式中只會儲存 JPEG。 + + 循環切換 RAW 模式 + 顯示 RAW 圖示 + 是否在螢幕上顯示圖示來循環切換 RAW 模式 + + 目前遠端: + 掃描 + + 陰影文字 + 普通文字 + 带背景色的文字 + + 正在儲存圖片… + + 完成全景 + 取消全景 + 已取消全景 + 無法建立全景圖片 + + 自動裁剪全景照片 + 在全景模式,是否裁剪照片沒有對齊的邊緣\n%s + + 拍攝全景照片時,請將裝置保持直向。點擊拍照按鈕後,向左或向右旋轉裝置,將居中的白色圓圈移到藍點上。拍攝新圖片後,繼續旋轉裝置以覆蓋依次顯示的每個新藍點。\n\n點擊打勾圖示來儲存全景照片,或者點擊打叉圖示取消全景照片。\n\n注意處理和儲存全景照片需要耗費一些時間。 + + 全景照片原圖 + 在全景模式是否儲存原圖。注意這會耗費更多的時間。也有選項可以儲存有助於回報全景問題的 XML 檔案。\n%s + + 不儲存原圖 + 儲存原圖 + 儲存原圖和除錯 XML + + ID + + 變更曝光補償的搜尋欄 + + 選擇照片的檔案格式。這會影響「標準」(非 RAW)的照片。注意 PNG 格式不是真的無損,而是以 100%% 品質從 JPEG 轉換過來。\n%s + + Camera API + 選擇 Camera2 API 來啟用額外的功能,例如手動曝光模式、對焦、白平衡和 RAW(如果裝置支援)。變更 API 會造成重新啟動\n%s + 原本的 camera API + Camera2 API + + 當偵測到聲音或是或音訊指令時拍照/錄影。 + 當啟用時,使用螢幕上的麥克風按鈕來開始/停止監聽。 + 注意音訊指令選項會使用 Android 語音辨識服務:當使用這個選項時, +音訊資料可能會可能會被傳送到遠端伺服器來進行語音辨識。 + \n%s + + 可以的話,當標記 GPS 位置時使用地址。 + 這個選項需要網路連線。 + 注意如果啟用這個選項,您的裝置需要透過網路將位置資訊到第三方 + 以將 GPS 座標轉換為地址。請參閱 https://developer.android.com/reference/android/location/Geocoder . + \n%s + + 點擊以對焦,按下藍色相機按鈕來拍照。 + \n\n要停用最大螢幕亮度,請到設定/螢幕 GUI/「強制最大亮度」。 + 要在 Android 5+ 儲存到 SD 卡,請到設定/更多相機控制/「使用 Storage Access Framework」。 + 請從設定點選「線上說明」取得更多說明。 + + 選擇遠端裝置 + 關閉 + 只隱藏螢幕上的虛擬按鈕 + 隱藏圖形化使用者介面 + 隱藏全部 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 9d36156f3763747dab375c2bfc19649435659058..29a5ea2725d3702834af9ca992a14d6c1d6912a3 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -115,18 +115,6 @@ 0 180 - - Off - Only hide on-screen virtual navigation buttons - Hide GUI - Hide everything - - - immersive_mode_off - immersive_mode_low_profile - immersive_mode_gui - immersive_mode_everything - Red Green @@ -559,19 +547,55 @@ @string/preference_video_log_off + @string/preference_video_rec709 + @string/preference_video_srgb @string/preference_video_log_fine @string/preference_video_log_low @string/preference_video_log_medium @string/preference_video_log_strong @string/preference_video_log_extra_strong + @string/preference_video_gamma + @string/preference_video_jtvideo + @string/preference_video_jtlog + @string/preference_video_jtlog2 off + rec709 + srgb fine low medium strong extra_strong + gamma + jtvideo + jtlog + jtlog2 + + + @string/preference_video_profile_gamma_1_6 + @string/preference_video_profile_gamma_1_8 + @string/preference_video_profile_gamma_1_9 + @string/preference_video_profile_gamma_2_0 + @string/preference_video_profile_gamma_2_1 + @string/preference_video_profile_gamma_2_2 + @string/preference_video_profile_gamma_2_3 + @string/preference_video_profile_gamma_2_4 + @string/preference_video_profile_gamma_2_6 + @string/preference_video_profile_gamma_2_8 + + + 1.6 + 1.8 + 1.9 + 2.0 + 2.1 + 2.2 + 2.3 + 2.4 + 2.6 + 2.8 @string/preference_record_audio_src_camcorder @@ -689,6 +713,9 @@ @string/preference_burst_mode_30x @string/preference_burst_mode_40x @string/preference_burst_mode_50x + @string/preference_burst_mode_100x + @string/preference_burst_mode_200x + @string/preference_burst_mode_500x @string/preference_burst_mode_unlimited @@ -702,6 +729,9 @@ 30 40 50 + 100 + 200 + 500 unlimited @@ -892,6 +922,11 @@ @string/preference_zebra_stripes_70pc @string/preference_zebra_stripes_80pc @string/preference_zebra_stripes_90pc + @string/preference_zebra_stripes_93pc + @string/preference_zebra_stripes_95pc + @string/preference_zebra_stripes_97pc + @string/preference_zebra_stripes_98pc + @string/preference_zebra_stripes_99pc @string/preference_zebra_stripes_100pc @@ -899,6 +934,11 @@ 179 204 230 + 237 + 242 + 247 + 250 + 252 255 @@ -927,4 +967,36 @@ preference_panorama_save_all preference_panorama_save_all_plus_debug + + @string/preference_immersive_mode_off + @string/preference_immersive_mode_low_profile + @string/preference_immersive_mode_navigation + @string/preference_immersive_mode_gui + @string/preference_immersive_mode_everything + + + immersive_mode_off + immersive_mode_low_profile + immersive_mode_navigation + immersive_mode_gui + immersive_mode_everything + + + @string/preference_zebra_stripes_color_black + @string/preference_zebra_stripes_color_red + @string/preference_zebra_stripes_color_orange + + + #ff000000 + #fff44336 + #ffff9800 + + + @string/preference_zebra_stripes_color_white + @string/preference_zebra_stripes_color_transparent + + + #ffffffff + #00000000 + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1eb90b97a8e8d5f6a1a48f7a2671b61af0b55de4..565681788a053f83f10bc8823619f6cc7f6d130a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ - #80000000 + #20000000 #20000000 #ff000000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36ff0a30ba8c8c8bf1624b2c92f39aeadd4d5e26..ca520f4e4570a307ce85cf6a44a4e3395987c084 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,7 +113,7 @@ Save location Folder to save the photo/video files in Use Storage Access Framework - Whether to use Storage Access Framework for saving photos and videos. This should be enabled (on Android 5+) to allow saving to external SD cards. + Whether to use Storage Access Framework for saving photos and videos. This should be enabled to allow saving to external SD cards. Save photo prefix The prefix to use for the save filenames for photos Save video prefix @@ -157,8 +157,8 @@ Grid Show a crop guide A crop guide displays a rectangle showing what the specified aspect ratio looks like - useful if you plan to crop the photo/video afterwards to a different aspect ratio. Requires WYSIWYG photo mode, or being in video mode.\n%s - Show \"toast\" messages - Whether to display the popup \"toast\" messages + Show on-screen messages + Whether to display temporary on-screen info messages Show thumbnail animation Display moving thumbnail animation when taking a photo Show border when taking photo @@ -200,12 +200,12 @@ Video resolution Force 4K UHD video (only works on some devices) Enable 3840x2160 resolution for video recording on back camera - this option is a hack that may allow 4K devices on 4K cameras that don\'t expose the option to 3rd party camera apps. This isn\'t guaranteed to work, please test before working. - Enable video stabilization - Video stabilization reduces the shaking due to the motion of the camera in both the preview and in recorded videos + Enable digital video stabilization + Video stabilization reduces the shaking due to the motion of the camera in both the preview and in recorded videos. This may be unnecessary if your device supports optical image stabilization (OIS). Video bitrate (approx) Set the approximate bitrate of videos (higher means better quality, but takes up more disk space; may cause video recording to fail if bitrate not supported)\n%s Video frame rate (approx) - Set the frame rate (FPS) of videos (may be approx, not guaranteed to be achieved, and may cause video recording to fail if frame rate not supported). Note that this setting is ignored for slow motion videos.\n%s + Set the frame rate (FPS) of videos (may be approx, not guaranteed to be achieved, and may cause video recording to fail if frame rate not supported). Please check resultant videos to find the actual frame rate used. Note that this setting is ignored for slow motion videos.\n%s Maximum duration of video The video recording will stop after the specified duration\n%s Restart video after max duration @@ -217,7 +217,7 @@ Record audio Record audio when recording video Audio source - Microphone to use for recording audio\n%s + Microphone to use for recording audio. Note that the exact behaviour of the options depends on how the option is implemented on your device.\n%s Audio channels Specify mono or stereo for recording audio (stereo only supported on some devices) Flash while recording video @@ -293,7 +293,7 @@ HDR Expo {} Exposure Bracketing - []]] + []]] Fast Burst NR Noise Reduction @@ -539,14 +539,14 @@ Allow photos whilst recording video Allows taking photos whilst recording videos. Disable this if there are problems with video recording with Camera2 API enabled on your device. - Video flat (log) profile - Enable a flat (log) profile for video mode\n%s - Off - Fine - Low - Medium - Strong - Extra strong + Video picture profiles + Set standard or flat picture profile for video mode\n%s + Default + Log (Fine) + Log (Low) + Log (Medium) + Log (Strong) + Log (Extra strong) Log profile Camcorder @@ -648,6 +648,9 @@ 30x 40x 50x + 100x + 200x + 500x Unlimited No delay @@ -826,6 +829,11 @@ 70% 80% 90% + 93% + 95% + 97% + 98% + 99% 100% Focus peaking @@ -886,7 +894,7 @@ Select the file format used for saving photos. This affects \"standard\" (not RAW) photos. Note that PNG format is not truly lossless, instead it is converted from a JPEG at 100%% quality.\n%s Camera API - Select Camera2 API to enables extra feature such as manual modes for exposure, focus, white balance, along with RAW (if supported by the device), but may not work properly on all devices. Changing the API will cause a restart.\n%s + Select Camera2 API to enable extra features such as manual modes for exposure, focus, white balance, along with RAW (if supported by the device). Changing the API will cause a restart.\n%s Original camera API Camera2 API @@ -908,17 +916,86 @@ For more help, click \"Online help\" from Settings. - - - Privacy policy - Tap to display privacy policy - Online privacy policy - - Camera accesses camera sensor and microphone data to fulfil its purpose as a camera. + Select remote device + + REC709 + sRGB + Gamma + JTVideo + JTLog + JTLog2 + Video gamma value + Gamma value to use for video if video picture profile is set to Gamma\n%s + 1.6 + 1.8 + 1.9 + 2.0 + 2.1 + 2.2 + 2.3 + 2.4 + 2.6 + 2.8 + + Off + Only dim on-screen virtual navigation buttons + Hide on-screen virtual navigation buttons + Hide GUI + Hide everything + + Ghost image opacity + Alpha value to use for ghost images. A lower value means more transparent, higher means more opaque.\n%s + + Zebra stripes foreground color + Color to use for the foreground stripe when showing zebra stripes.\n%s + Zebra stripes background color + Color to use for the background stripe when showing zebra stripes.\n%s + + Black + Red + Orange + Transparent + White + + External camera + Ultra-wide + + Switch to external camera + Switch camera + Switch between multiple cameras + Multiple cameras icon + If enabled, use separate buttons to switch between front/back cameras, and to switch between multiple front/back facing cameras. If disabled, the Switch Camera icon will cycle through all cameras. + + Show camera ID + Display the current camera ID number on screen + + Allow long press actions + Whether to allow long press actions (e.g., long press on gallery to change save location). + + Aperture + + 4K UHD + Bitrate + Frame rate + Slow motion + + Store yaw, pitch and roll + Store the yaw, pitch and roll of the device in the photo\'s Exif user comment (JPEG format only) + + + + Privacy policy + Tap to display privacy policy + Online privacy policy + + Open Camera accesses camera sensor and microphone data to fulfil its purpose as a camera. Microphone is also used for the optional \"Audio control\". - \nAccess to files is needed to save resultant photos and videos to your device. - \nLocation permission is required for the optional geotagging/stamp/subtitles and Bluetooth features. - \nSince Camera also uses operating system APIs, you should review relevant privacy policies + \nAccess to files is needed to save resultant files such as photos and videos to your device. + \nLocation permission is required for the optional geotagging features (for photos and videos, + including stamp and subtitles options). + When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files. + Location permission is also required to connect to Bluetooth remote control devices. + \nSince Open Camera also uses operating system APIs, you should review relevant privacy policies such as for your device, manufacturer, operating system and/or Google accounts. For example: \n*The optional voice control option uses the Android speech recognition service. When enabled, audio data is likely to be sent to remote servers by Android to perform speech recognition. @@ -928,7 +1005,7 @@ \n*Apps/services such as cloud services on your device may auto-upload photos and videos that are saved on your device. - + (Your audio may be sent to remote\n servers by Android to perform speech\n recognition.) @@ -943,20 +1020,30 @@ https://github.com/google/material-design-icons/ , https://google.github.io/material-design-icons/) under the Apache license version 2.0. Some icons include modifications. Tap here for full licence text. - Online licences - Please tap here for full online licensing information (opens in your browser). + Online licences + Please tap here for full online licensing information (opens in your browser). [This dialog is shown when Camera is updated. You can disable it under Settings/On screen GUI/Show What\'s New dialog.] - \n\nv1.47.3:\n + \n\nv1.48:\n
    -
  • Camera2 API option is now a \"list\" selection rather than a toggle switch. This option - should be copied over, but just in case, if you had enabled Camera2 API and Camera2 features - are no longer available, please check Settings/\"Camera API\".\n
  • -
  • Grids were being drawing too faintly.\n
  • -
  • Updated appearance of some icons.\n
  • +
  • New icon for multiple cameras. If your device has multiple front/back cameras, then + the existing icon to switch cameras will switch between front + and back camera; the new icon will instead cycle between the multiple front or back cameras. + If you prefer the old behaviour, disable + Settings/On screen GUI/\"Multiple cameras icon\". (Note, only supported for devices that expose + multiple cameras to third party applications.)\n
  • +
  • Auto-level feature now shows on-screen rectangle to show the frame of the resultant photo.\n
  • +
  • Aperture control, for devices that support this. (Camera2 API only.)\n
  • +
  • More custom video profiles.\n
  • +
  • New option for alpha value for ghost image option.\n
  • +
  • Options for zebra stripe colours.\n
  • +
  • Option for storing device\'s current yaw/pitch/roll in Exif user comment.\n
  • +
  • Option to disable long press actions.\n
  • +
  • Better use of full screen with wide aspect ratios on devices with on-screen navigation buttons.\n
  • +
  • Bug fixes and UI improvements.\n
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 3a58fe3474946a2c5c0300d6f66080df107fbe13..a9dbb28dc8677a6f9e1437acb364b4bb2e476755 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -198,12 +198,17 @@ android:entries="@array/preference_remote_type_entries" android:entryValues="@array/preference_remote_type_values" android:defaultValue="preference_remote_type_kraken" + android:dependency="preference_enable_remote" /> + android:title="@string/preference_select_remote" + android:dependency="preference_enable_remote" + > + + @@ -295,6 +302,13 @@ android:defaultValue="true" /> + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + - + + + + diff --git a/app/src/main/rs/histogram_compute.rs b/app/src/main/rs/histogram_compute.rs index 7ed381f5539f8f9fb9eb8de71f826a0ec8877a77..90be7d5a5eba40789d43a9bb94c176529e129915 100644 --- a/app/src/main/rs/histogram_compute.rs +++ b/app/src/main/rs/histogram_compute.rs @@ -85,6 +85,14 @@ void __attribute__((kernel)) histogram_compute_rgb(uchar4 in, uint32_t x, uint32 } int zebra_stripes_threshold = 255; +int zebra_stripes_foreground_r = 0; +int zebra_stripes_foreground_g = 0; +int zebra_stripes_foreground_b = 0; +int zebra_stripes_foreground_a = 255; +int zebra_stripes_background_r = 255; +int zebra_stripes_background_g = 255; +int zebra_stripes_background_b = 255; +int zebra_stripes_background_a = 255; int zebra_stripes_width = 40; uchar4 __attribute__((kernel)) generate_zebra_stripes(uchar4 in, uint32_t x, uint32_t y) { @@ -98,16 +106,17 @@ uchar4 __attribute__((kernel)) generate_zebra_stripes(uchar4 in, uint32_t x, uin out.a = 255;*/ int stripe = (x+y)/zebra_stripes_width; if( stripe % 2 == 0 ) { - out.r = 255; - out.g = 255; - out.b = 255; + out.r = zebra_stripes_background_r; + out.g = zebra_stripes_background_g; + out.b = zebra_stripes_background_b; + out.a = zebra_stripes_background_a; } else { - out.r = 0; - out.g = 0; - out.b = 0; + out.r = zebra_stripes_foreground_r; + out.g = zebra_stripes_foreground_g; + out.b = zebra_stripes_foreground_b; + out.a = zebra_stripes_foreground_a; } - out.a = 255; } else { out.r = 0; diff --git a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java index 4cf272eeb37be8955b9c4b05b76c48e63d5d038d..17b04cec03694a307a8397d1e9685ec97e8999a2 100644 --- a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java +++ b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java @@ -601,6 +601,7 @@ public class UnitTest { /** Duplicates the code in avg_brighter.rs for median filter, to test this. * Finds median of the supplied values, sorting by the alpha component. */ + @SuppressWarnings("UnusedAssignment") private float4 findMedian(float4 p0, float4 p1, float4 p2, float4 p3, float4 p4) { if( p0.a > p1.a ) { float4 temp_p = p0; diff --git a/build.gradle b/build.gradle index 17457e9c801d112f68bc973d4717530ca297b02c..40261aa712496ace21884b944df56de17bb4d081 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:3.6.2' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 21127bfd87fe70b9629ec55bfad1bf97e7cab047..a6111dc9b85c7e6eafe0f355c80f0636f7a5e0f2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Sun Sep 01 15:26:21 BST 2019 +#Sat Apr 11 14:09:12 BST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/makesrcarchive.bat b/makesrcarchive.bat index cb133fb907f131fd781f3aefacc28c6d25e1fe16..59d3995b88119d90f003ea379fc402735c24e124 100644 --- a/makesrcarchive.bat +++ b/makesrcarchive.bat @@ -23,6 +23,13 @@ copy %src%\app\build.gradle %dst%\app\ mkdir %dst%\gradle xcopy %src%\gradle %dst%\gradle /E /Y +REM We copy the inspectionProfiles as this stores which Android inspection warnings/errors we've disabled; although +REM note this isn't part of the Git repository, due lots of other files in .idea/ that we don't want to be part of the +REM project. +mkdir %dst%\.idea +mkdir %dst%\.idea\inspectionProfiles +xcopy %src%\.idea\inspectionProfiles %dst%\.idea\inspectionProfiles /E /Y + mkdir %dst%\_docs REM xcopy %src%\_docs %dst%\_docs /E /Y copy %src%\_docs\credits.html %dst%\_docs diff --git a/opencamera_source.txt b/opencamera_source.txt index a3e05c79dcae8a500965f8fef3e492f810f021f3..76102ed62884c1087276ff4a94e6f3e0dd584371 100644 --- a/opencamera_source.txt +++ b/opencamera_source.txt @@ -19,6 +19,28 @@ Preview: At a higher level, you might want to take the classes in Preview/ as we Application: You might prefer to use Open Camera in its entirety, as an Activity within your application (or possibly converting the MainActivity to a fragment). Consider whether Open Camera's Settings should be unified with the settings in your application. Accesses to SharedPreferences use the PreferencesKeys class to find the keys. +Android inspection warnings/errors +================================== + +The file .idea/inspectionProfiles/Project_Default.xml contains settings for Android inpsections (see Editor/Inspections). The following have been disabled: + +* AndroidLintUnusedResources - seems to give false information on res/ images, as well as complaining about renderscript files in build folders. +* AndroidLintLockedOrientationActivity, AndroidLintSourceLockedOrientationActivity - Although it is on my TODO to support portrait and landscape orientations, for now Open Camera handles the orientation of the UI itself for better performance (as is common with many camera applications). Also these errors are categorised under ChromeOS, which for now is unlikely to be a huge part of Open Camera's target market. +* CharsetObjectCanBeUsed - This seems fundamentally broken, as it'll recommend using StandardCharsets, and then give an error that that's not available before API level 19! E.g., see Charset.forName("UTF-8") in SettingsManager.saveSettings(). Strangely though it doesn't warn about the API level if we replace it in ImageSaver.saveImageNow(). +* BooleanMethodIsAlwaysInverted - Generates too many useless warnings. +* ConstantConditions - Unfortunately this seems broken when calling an Android SDK function where only a stub source is available, such that the inspection incorrectly things an exception is thrown. E.g., see calls to TouchUtils.clickView() in MainActivityTest, this inspection thinks that e.g. "i<8" in for loops is always true. +* EmptyMethod - Sometimes can be useful to explicitly have a subclassed empty method, to show I've intentionally left it empty rather than forgotten to consider it. +* EmptyStatementBody - These may be intentional. +* PointlessBooleanExpression - This complains too much about controlling codepaths with final boolean flags (e.g., "if( debug_switch && blah )"). +* SameParameterValue - A parameter may have been provided for future proofing even if callers currently only use one value. +* TrivialIf - This suggests replacing clear if statements with shorter but more convoluted boolean expressions. +* UnusedReturnValue - In some cases a function may return something even if not currently used, either for future proofing, or we used it in the past (and might use again in the future). Whilst a caller not checking the return can be an error, this check wouldn't pick that up so long as at least one caller checked the return. + +The following are enabled, but with modifications: + +* UnnecessaryLocalVariable - m_ignoreImmediatelyReturnedVariables is true. +* unused - parameter="protected", REPORT_PARAMETER_FOR_PUBLIC_METHODS is false (otherwise this complaints about parameters in overriden methods being unused); method="protected" (in some cases public unused methods may be added for future proofing). + Licence ======= @@ -30,4 +52,4 @@ Homepage: http://opencamera.org.uk/ Google Play download: https://play.google.com/store/apps/details?id=net.sourceforge.opencamera -Mark Harman 9 September 2018 +Mark Harman 27 February 2020