package de.jeisfeld.augendiagnoselib.activities;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.hardware.SensorManager;
import android.media.ExifInterface;
import android.os.AsyncTask;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.util.TypedValue;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationSet;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import de.jeisfeld.augendiagnoselib.Application;
import de.jeisfeld.augendiagnoselib.R;
import de.jeisfeld.augendiagnoselib.activities.OrganizeNewPhotosActivity.NextAction;
import de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView;
import de.jeisfeld.augendiagnoselib.components.PinchImageView;
import de.jeisfeld.augendiagnoselib.util.Camera1Handler;
import de.jeisfeld.augendiagnoselib.util.Camera2Handler;
import de.jeisfeld.augendiagnoselib.util.CameraHandler;
import de.jeisfeld.augendiagnoselib.util.DialogUtil;
import de.jeisfeld.augendiagnoselib.util.OrientationManager;
import de.jeisfeld.augendiagnoselib.util.OrientationManager.OrientationListener;
import de.jeisfeld.augendiagnoselib.util.OrientationManager.ScreenOrientation;
import de.jeisfeld.augendiagnoselib.util.PreferenceUtil;
import de.jeisfeld.augendiagnoselib.util.SystemUtil;
import de.jeisfeld.augendiagnoselib.util.TrackingUtil;
import de.jeisfeld.augendiagnoselib.util.TrackingUtil.Category;
import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto;
import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto.RightLeft;
import de.jeisfeld.augendiagnoselib.util.imagefile.FileUtil;
import de.jeisfeld.augendiagnoselib.util.imagefile.ImageUtil;
import de.jeisfeld.augendiagnoselib.util.imagefile.JpegMetadata;
import de.jeisfeld.augendiagnoselib.util.imagefile.JpegSynchronizationUtil;
import de.jeisfeld.augendiagnoselib.util.imagefile.MediaStoreUtil;
import de.jeisfeld.augendiagnoselib.util.imagefile.PupilAndIrisDetector;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static de.jeisfeld.augendiagnoselib.activities.CameraActivity.Action.CANCEL_AND_VIEW_IMAGES;
import static de.jeisfeld.augendiagnoselib.activities.CameraActivity.Action.CHECK_PHOTO;
import static de.jeisfeld.augendiagnoselib.activities.CameraActivity.Action.FINISH_CAMERA;
import static de.jeisfeld.augendiagnoselib.activities.CameraActivity.Action.RE_TAKE_PHOTO;
import static de.jeisfeld.augendiagnoselib.activities.CameraActivity.Action.TAKE_PHOTO;
import static de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto.RightLeft.LEFT;
import static de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto.RightLeft.RIGHT;
/**
* An activity to take pictures with the camera.
*/
public class CameraActivity extends StandardActivity {
/**
* The resource key for the folder where to store the photos.
*/
private static final String STRING_EXTRA_PHOTOFOLDER = "de.jeisfeld.augendiagnoselib.PHOTOFOLDER";
/**
* The resource key for the file to be re-taken.
*/
private static final String STRING_EXTRA_PHOTO_RIGHT = "de.jeisfeld.augendiagnoselib.PHOTO_RIGHT";
/**
* The resource key for the file to be re-taken.
*/
private static final String STRING_EXTRA_PHOTO_LEFT = "de.jeisfeld.augendiagnoselib.PHOTO_LEFT";
/**
* The size of the circle overlay bitmap.
*/
private static final int CIRCLE_BITMAP_SIZE = OverlayPinchImageView.OVERLAY_SIZE;
/**
* The maximum circle size.
*/
private static final int MAX_CIRCLE_RADIUS = 512;
/**
* The minimum circle size.
*/
private static final int MIN_CIRCLE_RADIUS = 128;
/**
* The default circle size.
*/
private static final int DEFAULT_CIRCLE_RADIUS = 384;
/**
* Activity String used for tracking.
*/
private static final String CAMERA = "Camera";
/**
* The available focus modes.
*/
private static List<FocusMode> mFocusModes;
/**
* The used flashlight modes.
*/
private static List<FlashMode> mFlashlightModes;
/**
* A flag indicating if zoom is available.
*/
private static boolean mIsZoomAvailable = false;
/**
* The current rightLeft in the activity.
*/
private Action mCurrentAction;
/**
* The current flashlight mode.
*/
private FlashMode mCurrentFlashlightMode;
/**
* The current focus mode.
*/
private FocusMode mCurrentFocusMode;
/**
* The current eye.
*/
private RightLeft mCurrentRightLeft;
/**
* The side of the last recorded eye.
*/
private RightLeft mLastRightLeft;
/**
* The temp file holding the right eye.
*/
@Nullable
private File mRightEyeFile = null;
/**
* The temp file holding the next photo for the right eye.
*/
@Nullable
private File mNewRightEyeFile = null;
/**
* The temp file holding the left eye.
*/
@Nullable
private File mLeftEyeFile = null;
/**
* The temp file holding the next photo for the left eye.
*/
@Nullable
private File mNewLeftEyeFile = null;
/**
* The folder where to store the photos.
*/
@Nullable
private File mPhotoFolder = null;
/**
* The right eye file coming as input to the activity.
*/
@Nullable
private File mInputRightFile = null;
/**
* The left eye file coming as input to the activity.
*/
@Nullable
private File mInputLeftFile = null;
/**
* An orientation manager used to track the orientation of the image.
*/
@Nullable
private OrientationManager mOrientationManager = null;
/**
* The current screen orientation.
*/
private ScreenOrientation mCurrentScreenOrientation;
/**
* The handler operating the camera.
*/
@Nullable
private CameraHandler mCameraHandler;
/**
* Timestamp for measuring the tracking duration.
*/
private long mTrackingTimestamp = 0;
/**
* Static helper method to start the activity for taking two photos to the input folder.
*
* @param activity The activity from which the activity is started.
* @param photoFolder The folder where to store the photos.
*/
public static void startActivity(@NonNull final Activity activity, final String photoFolder) {
Intent intent = new Intent(activity, CameraActivity.class);
if (photoFolder != null) {
intent.putExtra(STRING_EXTRA_PHOTOFOLDER, photoFolder);
}
activity.startActivity(intent);
}
/**
* Static helper method to start the activity for re-checking two images.
*
* @param activity The activity from which the activity is started.
* @param photoRight The path of the right eye image
* @param photoLeft The path of the left eye image
*/
public static void startActivity(@NonNull final Activity activity, final String photoRight, final String photoLeft) {
Intent intent = new Intent(activity, CameraActivity.class);
if (photoRight != null) {
intent.putExtra(STRING_EXTRA_PHOTO_RIGHT, photoRight);
}
if (photoLeft != null) {
intent.putExtra(STRING_EXTRA_PHOTO_LEFT, photoLeft);
}
activity.startActivity(intent);
}
@Override
public final void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (isCreationFailed()) {
return;
}
int permission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
if (permission == PackageManager.PERMISSION_GRANTED) {
setupActivity();
}
// StandardActivity will request for permission. If permission is granted, then onRequestPermissionsResult will setup the activity.
}
/**
* Do the basic setup of the activity. This is basically the key part of the onCreate method.
*/
private void setupActivity() {
setContentView(R.layout.activity_camera);
setCameraHandler();
configureMainButtons();
configureThumbButtons();
configureZoomCircleButton();
configureFlashlightButton();
// Focus button is configured after callback from CameraHandler.
int screenAppearance =
PreferenceUtil.getSharedPreferenceIntString(R.string.key_camera_screen_position, R.string.pref_default_camera_screen_position);
if (screenAppearance > 0) {
FrameLayout cameraOverallFrame = (FrameLayout) findViewById(R.id.camera_overall_frame);
int offset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 8, getResources().getDisplayMetrics()); // MAGIC_NUMBER
if (screenAppearance == 1) {
// value 1: left offset
cameraOverallFrame.setPadding(offset, 0, 0, 0);
}
else {
// value 2: right offset
cameraOverallFrame.setPadding(0, 0, offset, 0);
}
}
String photoFolderName = getIntent().getStringExtra(STRING_EXTRA_PHOTOFOLDER);
if (photoFolderName != null) {
mPhotoFolder = new File(photoFolderName);
}
String inputRightFileName = getIntent().getStringExtra(STRING_EXTRA_PHOTO_RIGHT);
String inputLeftFileName = getIntent().getStringExtra(STRING_EXTRA_PHOTO_LEFT);
// Handle the different scenarios based on input and based on existing temp files.
if (inputLeftFileName != null || inputRightFileName != null) {
if (inputRightFileName != null) {
mInputRightFile = new File(inputRightFileName);
}
if (inputLeftFileName != null) {
mInputLeftFile = new File(inputLeftFileName);
}
// Triggered by OrganizeNewPhotosActivity to update photos.
mPhotoFolder = mInputRightFile == null ? mInputLeftFile.getParentFile() : mInputRightFile.getParentFile();
if (FileUtil.getTempCameraFolder().equals(mPhotoFolder)) {
mPhotoFolder = null;
}
mLeftEyeFile = mInputLeftFile;
if (mLeftEyeFile != null) {
setThumbImage(mLeftEyeFile.getAbsolutePath(), LEFT);
}
mRightEyeFile = mInputRightFile;
if (mRightEyeFile != null) {
setThumbImage(mRightEyeFile.getAbsolutePath(), RIGHT);
}
if (mLeftEyeFile != null && mRightEyeFile != null) {
setAction(RE_TAKE_PHOTO, null);
}
else if (mLeftEyeFile == null) {
setAction(TAKE_PHOTO, LEFT);
}
else {
setAction(TAKE_PHOTO, RIGHT);
}
}
else {
File[] existingFiles = getTempCameraFiles();
if (existingFiles == null || existingFiles.length == 0 || mPhotoFolder != null) {
// This is the standard scenario.
boolean leftEyeFirst = PreferenceUtil.getSharedPreferenceBoolean(R.string.key_eye_sequence_choice);
setAction(TAKE_PHOTO, leftEyeFirst ? LEFT : RIGHT);
}
else if (existingFiles.length == 1) {
// one file already there. Assume that this is already taken and we only have to take the other one
if (mLeftEyeFile != null) {
setThumbImage(mLeftEyeFile.getAbsolutePath(), LEFT);
setAction(TAKE_PHOTO, RIGHT);
}
else {
setThumbImage(mRightEyeFile.getAbsolutePath(), RIGHT);
setAction(TAKE_PHOTO, LEFT);
}
}
else {
// both files are already there - switch to Organize.
OrganizeNewPhotosActivity.startActivity(this, FileUtil.getTempCameraFolder().getAbsolutePath(),
mLastRightLeft == RIGHT, NextAction.VIEW_IMAGES);
finish();
return;
}
}
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
int overlayCircleSize = PreferenceUtil.getSharedPreferenceInt(R.string.key_internal_camera_circle_size, DEFAULT_CIRCLE_RADIUS);
drawOverlayCircle(overlayCircleSize);
mOrientationManager = new OrientationManager(this, SensorManager.SENSOR_DELAY_NORMAL, new OrientationListener() {
@Override
public void onOrientationChange(final ScreenOrientation screenOrientation) {
mCurrentScreenOrientation = screenOrientation;
}
});
mOrientationManager.enable();
}
@Override
public final void onDestroy() {
cleanupTempFolder();
// mCameraHandler.stopPreview();
if (mOrientationManager != null) {
mOrientationManager.disable();
}
super.onDestroy();
}
@Override
public final void onResume() {
super.onResume();
if (mCameraHandler != null) {
mCameraHandler.startPreview();
}
mTrackingTimestamp = System.currentTimeMillis();
}
@Override
public final void onPause() {
if (mCameraHandler != null) {
mCameraHandler.stopPreview();
}
super.onPause();
TrackingUtil.sendTiming(Category.TIME_USAGE, CAMERA, null, System.currentTimeMillis() - mTrackingTimestamp);
}
@Override
protected final int getHelpResource() {
return 0;
}
/**
* Configure the main buttons of this activity.
*/
private void configureMainButtons() {
// Add a listener to the capture button
final Button captureButton = (Button) findViewById(R.id.buttonCameraTrigger);
captureButton.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(final View v) {
// get an image from the camera
captureButton.setEnabled(false);
mCameraHandler.takePicture();
TrackingUtil.sendEvent(Category.EVENT_USER, CAMERA, "Capture");
}
});
// Add listeners to the accept/decline button
Button acceptButton = (Button) findViewById(R.id.buttonCameraAccept);
acceptButton.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(final View v) {
// Analyze next required step
if (mCurrentRightLeft == RIGHT) {
if (mRightEyeFile != null && mRightEyeFile.exists()) {
// noinspection ResultOfMethodCallIgnored
mRightEyeFile.delete();
}
mRightEyeFile = mNewRightEyeFile;
mNewRightEyeFile = null;
mLastRightLeft = RIGHT;
if (mLeftEyeFile == null) {
setAction(TAKE_PHOTO, LEFT);
PupilAndIrisDetector.determineAndStoreIrisPosition(mRightEyeFile.getAbsolutePath());
}
else {
setAction(FINISH_CAMERA, null);
}
}
else {
if (mLeftEyeFile != null && mLeftEyeFile.exists()) {
// noinspection ResultOfMethodCallIgnored
mLeftEyeFile.delete();
}
mLeftEyeFile = mNewLeftEyeFile;
mNewLeftEyeFile = null;
mLastRightLeft = LEFT;
if (mRightEyeFile == null) {
setAction(TAKE_PHOTO, RIGHT);
PupilAndIrisDetector.determineAndStoreIrisPosition(mLeftEyeFile.getAbsolutePath());
}
else {
setAction(FINISH_CAMERA, null);
}
}
TrackingUtil.sendEvent(Category.EVENT_USER, CAMERA, "Accept");
}
});
Button declineButton = (Button) findViewById(R.id.buttonCameraDecline);
declineButton.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(final View v) {
if (mCurrentAction == CHECK_PHOTO) {
if (mCurrentRightLeft == RIGHT) {
if (mNewRightEyeFile != null && mNewRightEyeFile.exists()) {
// noinspection ResultOfMethodCallIgnored
mNewRightEyeFile.delete();
}
mNewRightEyeFile = null;
if (mRightEyeFile != null && mRightEyeFile.exists()) {
setThumbImage(mRightEyeFile.getAbsolutePath(), RIGHT);
}
else {
setThumbImage(null, RIGHT);
}
}
else {
if (mNewLeftEyeFile != null && mNewLeftEyeFile.exists()) {
// noinspection ResultOfMethodCallIgnored
mNewLeftEyeFile.delete();
}
mNewLeftEyeFile = null;
}
if (mLeftEyeFile != null && mLeftEyeFile.exists()) {
setThumbImage(mLeftEyeFile.getAbsolutePath(), LEFT);
}
else {
setThumbImage(null, LEFT);
}
setAction(TAKE_PHOTO, mCurrentRightLeft);
}
TrackingUtil.sendEvent(Category.EVENT_USER, CAMERA, "Decline");
}
});
Button returnButton = (Button) findViewById(R.id.buttonCameraReturn);
returnButton.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(final View v) {
setAction(FINISH_CAMERA, null);
}
});
// Add a listener to the view image button
Button viewImagesButton = (Button) findViewById(R.id.buttonCameraViewImages);
viewImagesButton.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(final View v) {
setAction(CANCEL_AND_VIEW_IMAGES, null);
}
});
// Hide application specific buttons
TypedArray hiddenButtons = getResources().obtainTypedArray(R.array.hidden_camera_buttons);
for (int i = 0; i < hiddenButtons.length(); i++) {
int id = hiddenButtons.getResourceId(i, 0);
if (id != 0) {
View view = findViewById(id);
if (view != null) {
view.setVisibility(GONE);
view.setEnabled(false);
}
}
}
hiddenButtons.recycle();
}
/**
* Configure configuration buttons in this activity.
*/
private void configureThumbButtons() {
LinearLayout cameraThumbRight = (LinearLayout) findViewById(R.id.camera_thumb_layout_right);
cameraThumbRight.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
setAction(TAKE_PHOTO, RIGHT);
}
});
LinearLayout cameraThumbLeft = (LinearLayout) findViewById(R.id.camera_thumb_layout_left);
cameraThumbLeft.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
setAction(TAKE_PHOTO, LEFT);
}
});
}
/**
* Configure the button for setting the overlay circle size.
*/
private void configureZoomCircleButton() {
Button overlayCircleButton = (Button) findViewById(R.id.buttonCameraZoomOverlayCircle);
overlayCircleButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
boolean isVisible = !PreferenceUtil.getSharedPreferenceBoolean(R.string.key_internal_camera_zoom_circle_seekbar_visibility);
PreferenceUtil.setSharedPreferenceBoolean(R.string.key_internal_camera_zoom_circle_seekbar_visibility, isVisible);
findViewById(R.id.seekbarCameraOverlayCircle).setVisibility(isVisible ? VISIBLE : GONE);
if (mIsZoomAvailable) {
findViewById(R.id.seekbarCameraZoom).setVisibility(isVisible ? VISIBLE : GONE);
}
}
});
boolean isVisible = PreferenceUtil.getSharedPreferenceBoolean(R.string.key_internal_camera_zoom_circle_seekbar_visibility);
findViewById(R.id.seekbarCameraOverlayCircle).setVisibility(isVisible ? VISIBLE : GONE);
if (mIsZoomAvailable) {
findViewById(R.id.seekbarCameraZoom).setVisibility(isVisible ? VISIBLE : GONE);
}
SeekBar overlayCircleSeekbar = (SeekBar) findViewById(R.id.seekbarCameraOverlayCircle);
overlayCircleSeekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
// do nothing.
}
@Override
public void onStartTrackingTouch(final SeekBar seekBar) {
// do nothing.
}
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) {
int size = (int) ((float) progress / seekBar.getMax() * MAX_CIRCLE_RADIUS);
if (size < MIN_CIRCLE_RADIUS) {
size = 0;
}
if (fromUser) {
PreferenceUtil.setSharedPreferenceInt(R.string.key_internal_camera_circle_size, size);
}
drawOverlayCircle(size);
}
});
int overlayCircleSize = PreferenceUtil.getSharedPreferenceInt(R.string.key_internal_camera_circle_size, DEFAULT_CIRCLE_RADIUS);
overlayCircleSeekbar.setProgress((int) ((float) overlayCircleSize / MAX_CIRCLE_RADIUS * overlayCircleSeekbar.getMax()));
SeekBar zoomSeekbar = (SeekBar) findViewById(R.id.seekbarCameraZoom);
zoomSeekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
// do nothing.
}
@Override
public void onStartTrackingTouch(final SeekBar seekBar) {
// do nothing.
}
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) {
PreferenceUtil.setSharedPreferenceInt(R.string.key_internal_camera_zoom_seekbar_progress, progress);
float relativeProgress = (float) progress / seekBar.getMax();
mCameraHandler.setRelativeZoom(relativeProgress);
}
});
zoomSeekbar.setProgress(PreferenceUtil.getSharedPreferenceInt(R.string.key_internal_camera_zoom_seekbar_progress, 0));
}
/**
* Configure the button for setting flashlight.
*/
private void configureFlashlightButton() {
Button flashlightButton = (Button) findViewById(R.id.buttonCameraFlashlight);
if (SystemUtil.hasFlashlight()) {
determineAvailableFlashlightModes();
String storedFlashlightString = PreferenceUtil.getSharedPreferenceString(R.string.key_internal_camera_flashlight_mode);
FlashMode storedFlashlightMode;
try {
storedFlashlightMode = FlashMode.valueOf(storedFlashlightString);
}
catch (Exception e) {
storedFlashlightMode = FlashMode.OFF;
PreferenceUtil.setSharedPreferenceString(R.string.key_internal_camera_flashlight_mode, storedFlashlightMode.toString());
}
if (!mFlashlightModes.contains(storedFlashlightMode)) {
storedFlashlightMode = FlashMode.OFF;
PreferenceUtil.setSharedPreferenceString(R.string.key_internal_camera_flashlight_mode, storedFlashlightMode.toString());
}
setFlashlightMode(storedFlashlightMode);
flashlightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
if (mFlashlightModes.size() > 0) {
int flashlightModeIndex = mFlashlightModes.indexOf(mCurrentFlashlightMode);
flashlightModeIndex = (flashlightModeIndex + 1) % mFlashlightModes.size();
FlashMode newFlashlightMode = mFlashlightModes.get(flashlightModeIndex);
PreferenceUtil.setSharedPreferenceString(R.string.key_internal_camera_flashlight_mode, newFlashlightMode.toString());
setFlashlightMode(newFlashlightMode);
}
}
});
}
else {
setFlashlightMode(null);
flashlightButton.setVisibility(GONE);
}
}
/**
* Configure the button for setting focus.
*/
private void configureFocusButton() {
String storedFocusModeString = PreferenceUtil.getSharedPreferenceString(R.string.key_internal_camera_focus_mode);
FocusMode storedFocusMode;
try {
storedFocusMode = FocusMode.valueOf(storedFocusModeString);
}
catch (Exception e) {
storedFocusMode = FocusMode.MACRO;
PreferenceUtil.setSharedPreferenceString(R.string.key_internal_camera_focus_mode, storedFocusMode.toString());
}
if (!mFocusModes.contains(storedFocusMode)) {
if (mFocusModes.contains(FocusMode.AUTO)) {
storedFocusMode = FocusMode.AUTO;
PreferenceUtil.setSharedPreferenceString(R.string.key_internal_camera_focus_mode, storedFocusMode.toString());
}
else if (mFocusModes.size() > 0) {
storedFocusMode = mFocusModes.get(0);
PreferenceUtil.setSharedPreferenceString(R.string.key_internal_camera_focus_mode, storedFocusMode.toString());
}
else {
storedFocusMode = null;
PreferenceUtil.removeSharedPreference(R.string.key_internal_camera_focus_mode);
}
}
setFocusMode(storedFocusMode);
Button focusButton = (Button) findViewById(R.id.buttonCameraFocus);
if (mFocusModes.size() < 2) {
focusButton.setVisibility(GONE);
return;
}
focusButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
if (mFocusModes.size() > 0) {
int focusModeIndex = mFocusModes.indexOf(mCurrentFocusMode);
focusModeIndex = (focusModeIndex + 1) % mFocusModes.size();
FocusMode newFocusMode = mFocusModes.get(focusModeIndex);
PreferenceUtil.setSharedPreferenceString(R.string.key_internal_camera_focus_mode, newFocusMode.toString());
setFocusMode(newFocusMode);
}
}
});
SeekBar focusSeekbar = (SeekBar) findViewById(R.id.seekbarCameraFocus);
focusSeekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
// do nothing.
}
@Override
public void onStartTrackingTouch(final SeekBar seekBar) {
// do nothing.
}
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && isCamera2()) {
Camera2Handler cameraHandler = (Camera2Handler) mCameraHandler;
PreferenceUtil.setSharedPreferenceInt(R.string.key_internal_camera_focal_distance_seekbar_progress, progress);
float relativeProgress = (float) progress / seekBar.getMax();
cameraHandler.setRelativeFocalDistance(1 - relativeProgress);
}
}
});
focusSeekbar.setProgress(PreferenceUtil.getSharedPreferenceInt(R.string.key_internal_camera_focal_distance_seekbar_progress, 0));
}
/**
* Change to the given action.
*
* @param action The new action.
* @param rightLeft the next eye side.
*/
private void setAction(@NonNull final Action action, final RightLeft rightLeft) {
mCurrentAction = action;
mCurrentRightLeft = rightLeft;
LinearLayout cameraThumbRight = (LinearLayout) findViewById(R.id.camera_thumb_layout_right);
LinearLayout cameraThumbLeft = (LinearLayout) findViewById(R.id.camera_thumb_layout_left);
Button buttonCapture = (Button) findViewById(R.id.buttonCameraTrigger);
Button buttonAccept = (Button) findViewById(R.id.buttonCameraAccept);
Button buttonDecline = (Button) findViewById(R.id.buttonCameraDecline);
Button buttonReturn = (Button) findViewById(R.id.buttonCameraReturn);
Button buttonViewImages = (Button) findViewById(R.id.buttonCameraViewImages);
ImageView imageViewReview = (ImageView) findViewById(R.id.camera_review);
FrameLayout cameraPreviewFrame = (FrameLayout) findViewById(R.id.camera_preview_frame);
LinearLayout cameraSettingsLayout = (LinearLayout) findViewById(R.id.cameraSettingsLayout);
switch (action) {
case TAKE_PHOTO:
updateFlashlight();
mCameraHandler.startPreview();
buttonCapture.setVisibility(VISIBLE);
buttonCapture.setEnabled(true);
buttonAccept.setVisibility(GONE);
buttonDecline.setVisibility(GONE);
buttonReturn.setVisibility(mLeftEyeFile == null && mRightEyeFile == null ? GONE : VISIBLE);
if (buttonViewImages.isEnabled() && buttonReturn.getVisibility() == GONE) {
buttonViewImages.setVisibility(VISIBLE);
}
cameraSettingsLayout.setVisibility(VISIBLE);
imageViewReview.setVisibility(GONE);
cameraPreviewFrame.setVisibility(VISIBLE);
cameraThumbLeft.setEnabled(true);
cameraThumbRight.setEnabled(true);
if (rightLeft == RIGHT) {
cameraThumbRight.setBackgroundResource(R.drawable.camera_thumb_background_highlighted);
cameraThumbLeft.setBackgroundResource(R.drawable.camera_thumb_background);
}
else {
cameraThumbRight.setBackgroundResource(R.drawable.camera_thumb_background);
cameraThumbLeft.setBackgroundResource(R.drawable.camera_thumb_background_highlighted);
}
break;
case CHECK_PHOTO:
buttonCapture.setVisibility(GONE);
buttonAccept.setVisibility(VISIBLE);
buttonDecline.setVisibility(VISIBLE);
buttonReturn.setVisibility(GONE);
buttonViewImages.setVisibility(INVISIBLE);
cameraSettingsLayout.setVisibility(INVISIBLE);
imageViewReview.setVisibility(VISIBLE);
cameraPreviewFrame.setVisibility(GONE);
cameraThumbLeft.setEnabled(false);
cameraThumbRight.setEnabled(false);
updateFlashlight();
break;
case RE_TAKE_PHOTO:
buttonCapture.setVisibility(GONE);
buttonAccept.setVisibility(GONE);
buttonDecline.setVisibility(VISIBLE);
buttonReturn.setVisibility(VISIBLE);
buttonViewImages.setVisibility(GONE);
cameraSettingsLayout.setVisibility(VISIBLE);
imageViewReview.setVisibility(GONE);
cameraPreviewFrame.setVisibility(VISIBLE);
cameraThumbLeft.setEnabled(true);
cameraThumbRight.setEnabled(true);
cameraThumbLeft.setBackgroundResource(R.drawable.camera_thumb_background);
cameraThumbRight.setBackgroundResource(R.drawable.camera_thumb_background);
updateFlashlight();
break;
case FINISH_CAMERA:
mCameraHandler.stopPreview();
cleanupTempFolder();
// move files to their target position
if (mInputLeftFile != null || mInputRightFile != null) {
if (mLeftEyeFile != null && !mLeftEyeFile.equals(mInputLeftFile)) {
File newLeftFile = mInputLeftFile == null ? new File(mInputRightFile.getParentFile(), mLeftEyeFile.getName()) : mInputLeftFile;
FileUtil.moveFile(mLeftEyeFile, newLeftFile);
// prevent cleanup
mLeftEyeFile = newLeftFile;
MediaStoreUtil.deleteThumbnail(newLeftFile.getAbsolutePath());
MediaStoreUtil.addPictureToMediaStore(newLeftFile.getAbsolutePath());
}
if (mRightEyeFile != null && !mRightEyeFile.equals(mInputRightFile)) {
File newRightFile = mInputRightFile == null ? new File(mInputLeftFile.getParentFile(), mRightEyeFile.getName()) : mInputRightFile;
FileUtil.moveFile(mRightEyeFile, newRightFile);
// prevent cleanup
mRightEyeFile = newRightFile;
MediaStoreUtil.deleteThumbnail(newRightFile.getAbsolutePath());
MediaStoreUtil.addPictureToMediaStore(newRightFile.getAbsolutePath());
}
}
else if (mPhotoFolder != null && mPhotoFolder.isDirectory()) {
if (mLeftEyeFile != null) {
FileUtil.moveFile(mLeftEyeFile, new File(mPhotoFolder, mLeftEyeFile.getName()));
}
if (mRightEyeFile != null) {
FileUtil.moveFile(mRightEyeFile, new File(mPhotoFolder, mRightEyeFile.getName()));
}
}
File organizeFolder = mPhotoFolder == null ? FileUtil.getTempCameraFolder() : mPhotoFolder;
OrganizeNewPhotosActivity.startActivity(this, organizeFolder.getAbsolutePath(),
mLastRightLeft == RIGHT, NextAction.VIEW_IMAGES);
finish();
return;
case CANCEL_AND_VIEW_IMAGES:
mCameraHandler.stopPreview();
cleanupTempFolder();
ListFoldersForDisplayActivity.startActivity(this);
finish();
break;
default:
break;
}
}
/**
* Update the flashlight mode.
*
* @param flashlightMode The new flashlight mode.
*/
private void setFlashlightMode(final FlashMode flashlightMode) {
mCurrentFlashlightMode = flashlightMode;
updateFlashlight();
}
/**
* Update the focus mode.
*
* @param focusMode The new focus mode.
*/
private void setFocusMode(final FocusMode focusMode) {
mCurrentFocusMode = focusMode;
SeekBar seekbarCameraFocus = (SeekBar) findViewById(R.id.seekbarCameraFocus);
seekbarCameraFocus.setVisibility(mCurrentFocusMode == FocusMode.MANUAL ? VISIBLE : GONE);
mCameraHandler.setFocusMode(mCurrentFocusMode);
Button buttonCameraFocus = (Button) findViewById(R.id.buttonCameraFocus);
if (mCurrentFocusMode == null) {
buttonCameraFocus.setVisibility(GONE);
}
else {
buttonCameraFocus.setText(mCurrentFocusMode.toDisplayString());
}
}
/**
* Remove unused files from the temp folder.
*/
private void cleanupTempFolder() {
File[] tempFiles = FileUtil.getTempCameraFiles();
for (File file : tempFiles) {
if (!file.equals(mRightEyeFile) & !file.equals(mLeftEyeFile)) {
// noinspection ResultOfMethodCallIgnored
file.delete();
}
}
}
/**
* Get all temp camera files and link them to the app.
*
* @return The list of existing temp files.
*/
private File[] getTempCameraFiles() {
File[] existingFiles = FileUtil.getTempCameraFiles();
boolean leftEyeFirst = PreferenceUtil.getSharedPreferenceBoolean(R.string.key_eye_sequence_choice);
RightLeft rightLeft = existingFiles.length == 0 ? null : new EyePhoto(existingFiles[0]).getRightLeft();
boolean firstFileIsLeftEye = rightLeft == LEFT || rightLeft == null && leftEyeFirst;
if (existingFiles.length == 1) {
if (firstFileIsLeftEye) {
mLeftEyeFile = existingFiles[0];
}
else {
mRightEyeFile = existingFiles[0];
}
}
else if (existingFiles.length >= 2) {
mRightEyeFile = firstFileIsLeftEye ? existingFiles[1] : existingFiles[0];
mLeftEyeFile = firstFileIsLeftEye ? existingFiles[0] : existingFiles[1];
}
return existingFiles;
}
/**
* Set the thumb image with a byte array.
*
* @param data The data representing the bitmap.
*/
private void setThumbImage(@NonNull final byte[] data) {
ImageView imageView = (ImageView) findViewById(mCurrentRightLeft == RIGHT ? R.id.camera_thumb_image_right : R.id.camera_thumb_image_left);
Bitmap bitmap = ImageUtil.getImageBitmap(data, getResources().getDimensionPixelSize(R.dimen.camera_thumb_size));
imageView.setImageBitmap(bitmap);
}
/**
* Set the thumb image from a file.
*
* @param file The file to be put in the thumb.
* @param rightLeft The side of the eye
*/
private void setThumbImage(@Nullable final String file, final RightLeft rightLeft) {
ImageView imageView = (ImageView) findViewById(rightLeft == RIGHT ? R.id.camera_thumb_image_right : R.id.camera_thumb_image_left);
if (file != null) {
Bitmap bitmap = ImageUtil.getImageBitmap(file, getResources().getDimensionPixelSize(R.dimen.camera_thumb_size));
imageView.setImageBitmap(bitmap);
}
else {
imageView.setImageResource(rightLeft == RIGHT ? R.drawable.icon_eye_right : R.drawable.icon_eye_left);
}
}
/**
* Show the captured image for preview as fixed image.
*
* @param data The data representing the image.
*/
private void setReviewImage(@NonNull final byte[] data) {
PinchImageView imageView = (PinchImageView) findViewById(R.id.camera_review);
Bitmap bitmap = ImageUtil.getImageBitmap(data, findViewById(R.id.camera_preview_frame).getWidth());
imageView.setImage(bitmap);
}
/**
* Show a flashlight in the preview.
*/
private void animateFlash() {
final View flashView = findViewById(R.id.camera_flash);
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new DecelerateInterpolator());
fadeOut.setDuration(500); // MAGIC_NUMBER
fadeOut.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(final Animation animation) {
// do nothing
}
@Override
public void onAnimationRepeat(final Animation animation) {
// do nothing
}
@Override
public void onAnimationEnd(final Animation animation) {
flashView.setVisibility(GONE);
}
});
AnimationSet animation = new AnimationSet(false);
animation.addAnimation(fadeOut);
flashView.setVisibility(VISIBLE);
flashView.startAnimation(animation);
}
/**
* Draw the overlay circle.
*
* @param circleRadius The circle radius.
*/
private void drawOverlayCircle(final int circleRadius) {
Bitmap overlayBitmap = Bitmap.createBitmap(CIRCLE_BITMAP_SIZE, CIRCLE_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlayBitmap);
if (circleRadius > 0) {
Paint paint = new Paint();
paint.setAntiAlias(true);
int overlayColor = PreferenceUtil.getSharedPreferenceInt(R.string.key_overlay_color, Color.RED);
paint.setColor(overlayColor);
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(5); // MAGIC_NUMBER
canvas.drawCircle(CIRCLE_BITMAP_SIZE / 2, CIRCLE_BITMAP_SIZE / 2, circleRadius, paint);
}
ImageView overlayView = (ImageView) findViewById(R.id.camera_overlay);
overlayView.setImageBitmap(overlayBitmap);
}
/**
* Update the flashlight button and set the flashlight mode.
*/
private void updateFlashlight() {
Button flashlightButton = (Button) findViewById(R.id.buttonCameraFlashlight);
if (FlashMode.OFF.equals(mCurrentFlashlightMode)) {
flashlightButton.setBackgroundResource(R.drawable.circlebutton_noflash);
}
else if (FlashMode.ON.equals(mCurrentFlashlightMode)) {
flashlightButton.setBackgroundResource(R.drawable.circlebutton_flash);
}
else if (FlashMode.TORCH.equals(mCurrentFlashlightMode)) {
flashlightButton.setBackgroundResource(R.drawable.circlebutton_torch);
}
if (mCurrentFlashlightMode != null) {
if (mCurrentAction != Action.TAKE_PHOTO && mCurrentFlashlightMode == FlashMode.TORCH) {
mCameraHandler.setFlashlightMode(FlashMode.OFF);
}
else {
mCameraHandler.setFlashlightMode(mCurrentFlashlightMode);
}
}
}
/**
* Get the exif orientation to be applied.
*
* @return The orientation angle.
*/
private short getExifAngle() {
if (mCurrentScreenOrientation == null) {
return ExifInterface.ORIENTATION_NORMAL;
}
switch (mCurrentScreenOrientation) {
case LANDSCAPE:
return ExifInterface.ORIENTATION_NORMAL;
case PORTRAIT:
return ExifInterface.ORIENTATION_ROTATE_90;
case REVERSED_LANDSCAPE:
return ExifInterface.ORIENTATION_ROTATE_180;
case REVERSED_PORTRAIT:
return ExifInterface.ORIENTATION_ROTATE_270;
default:
return ExifInterface.ORIENTATION_NORMAL;
}
}
/**
* The callback called when pictures are taken.
*/
@Nullable
private final CameraCallback mOnPictureTakenHandler = new CameraCallback() {
@Override
public void onTakingPicture() {
runOnUiThread(new Runnable() {
@Override
public void run() {
animateFlash();
}
});
}
@Override
public void onPictureTaken(@NonNull final byte[] data) {
short exifAngle = getExifAngle();
File imageFile = FileUtil.getTempJpegFile();
JpegMetadata metadata = null;
if (mCurrentRightLeft == RIGHT) {
mNewRightEyeFile = imageFile;
if (mInputRightFile != null) {
// Keep metadata from input
metadata = JpegSynchronizationUtil.getJpegMetadata(mInputRightFile.getAbsolutePath());
}
}
else {
mNewLeftEyeFile = imageFile;
if (mInputLeftFile != null) {
// Keep metadata from input
metadata = JpegSynchronizationUtil.getJpegMetadata(mInputLeftFile.getAbsolutePath());
}
}
if (metadata == null) {
metadata = new JpegMetadata();
metadata.setRightLeft(mCurrentRightLeft);
metadata.setComment("");
metadata.setOrganizeDate(new Date());
}
metadata.setOrientation(exifAngle);
int overlayCircleRadius = PreferenceUtil.getSharedPreferenceInt(R.string.key_internal_camera_circle_size, DEFAULT_CIRCLE_RADIUS);
if (overlayCircleRadius > 0) {
metadata.setXCenter(0.5f); // MAGIC_NUMBER
metadata.setYCenter(0.5f); // MAGIC_NUMBER
metadata.setOverlayScaleFactor(((float) overlayCircleRadius) / CIRCLE_BITMAP_SIZE * getDefaultOverlayScaleFactor());
metadata.addFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY);
}
// save photo
new SavePhotoTask(data, mCurrentRightLeft, metadata).execute(imageFile);
runOnUiThread(new Runnable() {
@Override
public void run() {
setThumbImage(data);
setReviewImage(data);
setAction(CHECK_PHOTO, mCurrentRightLeft);
}
});
}
@Override
public void onCameraError(final String message, final String shortMessage, @Nullable final Throwable e) {
String messageString = message;
if (e == null) {
Log.e(Application.TAG, message);
}
else {
Log.e(Application.TAG, message, e);
messageString += "\n" + e.toString();
}
boolean isCamera2Api = mCameraHandler instanceof Camera2Handler;
boolean wasCamera2Successful = PreferenceUtil.getSharedPreferenceBoolean(R.string.key_internal_camera2_successful);
if (isCamera2Api && !wasCamera2Successful) {
// Reconfigure to Camera 1 API
PreferenceUtil.setSharedPreferenceIntString(R.string.key_camera_api_version, 1);
DialogUtil.displayError(CameraActivity.this, R.string.message_dialog_failed_to_use_camera2, true, messageString);
}
else {
DialogUtil.displayError(CameraActivity.this, R.string.message_dialog_failed_to_access_camera, true, messageString);
}
if (e != null) {
TrackingUtil.sendException(shortMessage, e);
}
}
@Override
public void updateAvailableModes(final List<FocusMode> focusModes) {
mFocusModes = focusModes;
configureFocusButton();
}
@Override
public void updateAvailableZoom(final boolean isZoomAvailable) {
mIsZoomAvailable = isZoomAvailable;
configureZoomCircleButton();
}
};
/**
* Get the default scale factor of the overlay (dependent on the surface).
*
* @return The default scale factor of the overlay.
*/
private float getDefaultOverlayScaleFactor() {
View surfaceView = findViewById(R.id.camera_preview_frame);
int height = surfaceView.getHeight();
int width = surfaceView.getWidth();
// Factor 8/3 due to 75% size of base overlay circle.
// Math/min factor due to strange implementation in OverlayPinchImageView.
return ((float) Math.min(width, height)) / Math.max(width, height) * 8 / 3; // MAGIC_NUMBER
}
/**
* Get the list of available flashlight modes.
*/
private void determineAvailableFlashlightModes() {
boolean enableFlashlight = PreferenceUtil.getSharedPreferenceBoolean(R.string.key_enable_flash);
if (enableFlashlight) {
mFlashlightModes = Arrays.asList(FlashMode.OFF, FlashMode.ON, FlashMode.TORCH);
}
else {
mFlashlightModes = Arrays.asList(FlashMode.OFF, FlashMode.TORCH);
}
}
/**
* Set the camera handler.
*/
private void setCameraHandler() {
SurfaceView camera1View = (SurfaceView) findViewById(R.id.camera1_preview);
TextureView camera2View = (TextureView) findViewById(R.id.camera2_preview);
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && isCamera2()) {
mCameraHandler = new Camera2Handler(this, (FrameLayout) findViewById(R.id.camera_preview_frame), camera2View, mOnPictureTakenHandler);
camera1View.setVisibility(GONE);
camera2View.setVisibility(VISIBLE);
}
else {
mCameraHandler = new Camera1Handler((FrameLayout) findViewById(R.id.camera_preview_frame), camera1View, mOnPictureTakenHandler);
camera1View.setVisibility(VISIBLE);
camera2View.setVisibility(GONE);
}
}
/**
* Get information if Camera2 API is used.
*
* @return true if Camera2 API is used.
*/
private boolean isCamera2() {
int cameraApiVersion = PreferenceUtil.getSharedPreferenceIntString(R.string.key_camera_api_version, null);
return cameraApiVersion == 2;
}
@Override
protected final String[] getRequiredPermissions() {
return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
}
@Override
protected final int getPermissionInfoResource() {
return R.string.message_dialog_confirm_need_camera_permission;
}
@Override
public final void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
if (requestCode == REQUEST_CODE_PERMISSION) {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
setupActivity();
}
else {
finish();
}
}
}
/**
* The task responsible for saving the picture.
*/
private final class SavePhotoTask extends AsyncTask<File, String, File> {
/**
* The data to be saved.
*/
private final byte[] mImageData;
/**
* The side of the eye to be saved.
*/
private final RightLeft mRightLeft;
/**
* The metadata to be stored.
*/
private final JpegMetadata mMetadata;
/**
* Constructor, passing the data to be saved.
*
* @param data The data to be saved.
* @param rightLeft The side of the eye to be saved.
* @param metadata Metadata to be stored in the photo.
*/
private SavePhotoTask(final byte[] data, final RightLeft rightLeft, final JpegMetadata metadata) {
this.mImageData = data;
this.mRightLeft = rightLeft;
this.mMetadata = metadata;
}
@Override
protected File doInBackground(final File... imageFiles) {
File imageFile = imageFiles[0];
try {
FileOutputStream fos = new FileOutputStream(imageFile.getAbsolutePath());
fos.write(mImageData);
fos.close();
if (mMetadata != null) {
JpegSynchronizationUtil.storeJpegMetadata(imageFile.getAbsolutePath(), mMetadata);
}
}
catch (java.io.IOException e) {
Log.e(Application.TAG, "Exception when saving photo", e);
}
return imageFile;
}
@Override
protected void onPostExecute(@NonNull final File imageFile) {
Log.d(Application.TAG, "Finished saving image " + imageFile.getName() + " - " + mRightLeft);
}
}
/**
* Handler called after the picture is taken.
*/
public interface CameraCallback {
/**
* Callback called just when the picture is taken.
*/
void onTakingPicture();
/**
* Callback called after the picture is taken.
*
* @param data The image data.
*/
void onPictureTaken(byte[] data);
/**
* Callback called on fatal camera errors.
*
* @param message The error message as String
* @param shortMessage a short form of the message (for analytics).
* @param e The exception
*/
void onCameraError(String message, String shortMessage, Throwable e);
/**
* Give information which focus modes and flash modes are supported by the camera.
*
* @param focusModes The supported focus modes.
*/
void updateAvailableModes(List<FocusMode> focusModes);
/**
* Give information if zoom is available.
*
* @param isZoomAvailable true if zoom is available.
*/
void updateAvailableZoom(boolean isZoomAvailable);
}
/**
* Enumeration for holding the current rightLeft that the activity is doing.
*/
enum Action {
/**
* Capture a photo.
*/
TAKE_PHOTO,
/**
* Check a photo.
*/
CHECK_PHOTO,
/**
* Finish the camera activity.
*/
FINISH_CAMERA,
/**
* Cancel and go to the view images activity.
*/
CANCEL_AND_VIEW_IMAGES,
/**
* Make an optional re-take of a photo.
*/
RE_TAKE_PHOTO
}
/**
* Enumeration for modes of the camera flash.
*/
public enum FlashMode {
/**
* The flash is off.
*/
OFF,
/**
* The flash is used when taking picture.
*/
ON,
/**
* The flash is permanently on.
*/
TORCH
}
/**
* Enumeration for modes of the camera focus.
*/
public enum FocusMode {
/**
* Continuous autofocus.
*/
CONTINUOUS,
/**
* Autofocus.
*/
AUTO,
/**
* Macro.
*/
MACRO,
/**
* Manual focus.
*/
MANUAL;
/**
* Convert into a display string, to be used in the GUI.
*
* @return the display string.
*/
public final String toDisplayString() {
switch (this) {
case CONTINUOUS:
return "AUTO\n∞";
default:
return toString();
}
}
}
}