Java tutorial
/********************************************************************************* * DocScan is a Android app for document scanning. * * Author: Fabian Hollaus, Florian Kleber, Markus Diem * Organization: TU Wien, Computer Vision Lab * Date created: 21. July 2016 * * This file is part of DocScan. * * DocScan is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * DocScan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with DocScan. If not, see <http://www.gnu.org/licenses/>. *********************************************************************************/ package at.ac.tuwien.caa.docscan.ui; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.hardware.Camera; import android.location.Location; import android.media.ExifInterface; import android.media.MediaScannerConnection; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.support.design.widget.NavigationView; import android.support.v4.app.ActivityCompat; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AlertDialog; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.Display; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import org.opencv.android.OpenCVLoader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; import at.ac.tuwien.caa.docscan.R; import at.ac.tuwien.caa.docscan.camera.CameraPaintLayout; import at.ac.tuwien.caa.docscan.camera.CameraPreview; import at.ac.tuwien.caa.docscan.camera.DebugViewFragment; import at.ac.tuwien.caa.docscan.camera.GPS; import at.ac.tuwien.caa.docscan.camera.LocationHandler; import at.ac.tuwien.caa.docscan.camera.NativeWrapper; import at.ac.tuwien.caa.docscan.camera.PaintView; import at.ac.tuwien.caa.docscan.camera.TaskTimer; import at.ac.tuwien.caa.docscan.camera.cv.CVResult; import at.ac.tuwien.caa.docscan.camera.cv.ChangeDetector; import at.ac.tuwien.caa.docscan.camera.cv.DkPolyRect; import at.ac.tuwien.caa.docscan.camera.cv.Patch; import at.ac.tuwien.caa.docscan.logic.AppState; import at.ac.tuwien.caa.docscan.logic.DataLog; import at.ac.tuwien.caa.docscan.sync.SyncInfo; import static at.ac.tuwien.caa.docscan.camera.TaskTimer.TaskType.FLIP_SHOT_TIME; import static at.ac.tuwien.caa.docscan.camera.TaskTimer.TaskType.PAGE_SEGMENTATION; import static at.ac.tuwien.caa.docscan.camera.TaskTimer.TaskType.SHOT_TIME; /** * The main class of the app. It is responsible for creating the other views and handling * callbacks from the created views as well as user input. */ public class CameraActivity extends BaseNavigationActivity implements TaskTimer.TimerCallbacks, CameraPreview.CVCallback, CameraPreview.CameraPreviewCallback, CVResult.CVResultCallback, MediaScannerConnection.MediaScannerConnectionClient, PopupMenu.OnMenuItemClickListener, AdapterView.OnItemSelectedListener { private static final String TAG = "CameraActivity"; private static final String FLASH_MODE_KEY = "flashMode"; // used for saving the current flash status private static final String DEBUG_VIEW_FRAGMENT = "DebugViewFragment"; private static final int PERMISSION_READ_EXTERNAL_STORAGE = 0; private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 1; private static final int PERMISSION_ACCESS_FINE_LOCATION = 2; @SuppressWarnings("deprecation") private Camera.PictureCallback mPictureCallback; private ImageButton mGalleryButton; private TaskTimer mTaskTimer; private CameraPreview mCameraPreview; private PaintView mPaintView; private TextView mCounterView; private boolean mShowCounter = true; private CVResult mCVResult; // Debugging variables: private DebugViewFragment mDebugViewFragment; private boolean mIsDebugViewEnabled; private static Context mContext; private int mCameraOrientation; private DrawerLayout mDrawerLayout; private ActionBarDrawerToggle mDrawerToggle; private MediaScannerConnection mMediaScannerConnection; private boolean mIsPictureSafe; private boolean mIsSaving = false; private TextView mTextView; private MenuItem mFlashMenuItem; private Drawable mManualShootDrawable, mAutoShootDrawable, mFlashOffDrawable, mFlashOnDrawable, mFlashAutoDrawable, mFlashTorchDrawable; private boolean mIsSeriesMode = false; private boolean mIsSeriesModePaused = false; private long mStartTime; // We hold here a reference to the popupmenu and the list, because we are not sure what is first initialized: private List<String> mFlashModes; private PopupMenu mFlashPopupMenu; private byte[] mPictureData; private Drawable mGalleryButtonDrawable; private ProgressBar mProgressBar; private final static int SINGLE_POS = 0; private final static int SERIES_POS = 1; private TaskTimer.TimerCallbacks mTimerCallbacks; private static Date mLastTimeStamp; private int mMaxFrameCnt = 0; private DkPolyRect[] mLastDkPolyRects; private Toast mToast; /** * Static initialization of the OpenCV and docscan-native modules. */ static { // We need this for Android 4: if (!OpenCVLoader.initDebug()) { Log.d(TAG, "Error while initializing OpenCV."); } else { System.loadLibrary("opencv_java3"); System.loadLibrary("docscan-native"); } } private long mLastTime; private boolean mItemSelectedAutomatically = false; // ================= start: methods from the Activity lifecycle ================= /** * Creates the camera Activity. * * @param savedInstanceState saved instance. */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // Open the log file at app startup: if (AppState.isDataLogged()) DataLog.getInstance().readLog(this); initActivity(); mContext = this; } /** * Stops the camera and the paint view thread. */ @Override public void onPause() { if (mPaintView != null) mPaintView.pause(); if (mCameraPreview != null) mCameraPreview.pause(); // MovementDetector.getInstance(this.getApplicationContext()).stop(); savePreferences(); // Save the log file: if (AppState.isDataLogged()) DataLog.getInstance().writeLog(this); super.onPause(); } /** * Stops the camera. */ @Override public void onStop() { super.onStop(); mCameraPreview.stop(); } /** * Called after the Activity resumes. Resumes the camera and the the paint view thread. */ @Override public void onResume() { super.onResume(); // Resume camera access: if (mCameraPreview != null) mCameraPreview.resume(); // Resume drawing thread: if (mPaintView != null) mPaintView.resume(); // MovementDetector.getInstance(this.getApplicationContext()).start(); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { // Save the current flash mode // if (mCameraPreview != null) // savedInstanceState.putString(FLASH_MODE_KEY, mCameraPreview.getFlashMode()); // Always call the superclass so it can save the view hierarchy state super.onSaveInstanceState(savedInstanceState); } // ================= end: methods from the Activity lifecyle ================= /** * Initializes the activity. */ private void initActivity() { setContentView(R.layout.activity_camera); mCVResult = new CVResult(this); mCameraPreview = (CameraPreview) findViewById(R.id.camera_view); mPaintView = (PaintView) findViewById(R.id.paint_view); if (mPaintView != null) mPaintView.setCVResult(mCVResult); mCounterView = (TextView) findViewById(R.id.counter_view); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); setupToolbar(); setupNavigationDrawer(); setupDebugView(); // Show the debug view: (TODO: This should be not the case for releases) // showDebugView(); // This is used to measure execution time of time intense tasks: mTaskTimer = new TaskTimer(); // initCameraControlLayout(); initDrawables(); initPictureCallback(); initButtons(); requestLocation(); loadPreferences(); } private void loadPreferences() { // Debug view: // Concerning series mode: SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); boolean seriesModeDefault = getResources().getBoolean(R.bool.series_mode_default); mIsSeriesMode = sharedPref.getBoolean(getString(R.string.series_mode_key), seriesModeDefault); boolean seriesModePausedDefault = getResources().getBoolean(R.bool.series_mode_paused_default); mIsSeriesModePaused = sharedPref.getBoolean(getString(R.string.series_mode_paused_key), seriesModePausedDefault); showShootModeToast(); updateMode(); updateShootModeSpinner(); } private void savePreferences() { // Concerning series mode: SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPref.edit(); editor.putBoolean(getString(R.string.series_mode_key), mIsSeriesMode); editor.putBoolean(getString(R.string.series_mode_paused_key), mIsSeriesModePaused); editor.commit(); } /** * This function accesses the hardware buttons (like volume buttons). We need this access, * because shutter remotes emulate such a key press over bluetooth. * @param keyCode * @param event * @return */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { takePicture(); } else if (keyCode == KeyEvent.KEYCODE_BACK) { super.onBackPressed(); } return true; } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.actionbar_menu, menu); mFlashMenuItem = menu.findItem(R.id.flash_mode_item); // The flash menu item is not visible at the beginning ('weak' devices might have no flash) if (mFlashModes != null) mFlashMenuItem.setVisible(true); return true; } @SuppressWarnings("deprecation") private void initDrawables() { mAutoShootDrawable = getResources().getDrawable(R.drawable.auto_shoot); mManualShootDrawable = getResources().getDrawable(R.drawable.manual_auto); mFlashAutoDrawable = getResources().getDrawable(R.drawable.ic_flash_auto); mFlashOffDrawable = getResources().getDrawable(R.drawable.ic_flash_off); mFlashOnDrawable = getResources().getDrawable(R.drawable.ic_flash_on); mFlashTorchDrawable = getResources().getDrawable(R.drawable.ic_torch); } /** * Called after permission has been given or has been rejected. This is necessary on Android M * and younger Android systems. * * @param requestCode Request code * @param permissions Permission * @param grantResults results */ @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { boolean isPermissionGiven = (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED); // initCamera(); switch (requestCode) { case PERMISSION_WRITE_EXTERNAL_STORAGE: if (isPermissionGiven && mPictureData != null) savePicture(mPictureData); break; case PERMISSION_ACCESS_FINE_LOCATION: if (isPermissionGiven) startLocationAccess(); break; } } // ================= start: methods for opening the gallery ================= /** * Connects the gallery button with its OnClickListener. */ private void initGalleryCallback() { mGalleryButton = (ImageButton) findViewById(R.id.gallery_button); mProgressBar = (ProgressBar) findViewById(R.id.saving_progressbar); mGalleryButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { openGallery(); } }); } /** * Opens the MediaScanner (if permission is given) to scan for saved pictures. */ private void openGallery() { int currentApiVersion = android.os.Build.VERSION.SDK_INT; if (currentApiVersion >= Build.VERSION_CODES.M) requestFileOpen(); else startScan(); } /** * Request to read the external storage. This method is used to enable file saving in Android >= marshmallow * (Android 6), since in this version external file opening is not allowed without user permission. */ @TargetApi(16) private void requestFileOpen() { // Check permission if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // ask for permission: ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, PERMISSION_READ_EXTERNAL_STORAGE); } else startScan(); } /** * Starts the MediaScanner. */ private void startScan() { if (mMediaScannerConnection != null) mMediaScannerConnection.disconnect(); mMediaScannerConnection = new MediaScannerConnection(this, this); mMediaScannerConnection.connect(); } /** * Tells the MediaScanner where the directory of DocScan pictures is and tells it to scan the * most recent file. */ @Override public void onMediaScannerConnected() { File mediaStorageDir = getMediaStorageDir(getResources().getString(R.string.app_name)); if (mediaStorageDir == null) { showNoFileFoundDialog(); return; } String[] files = mediaStorageDir.list(); if (files == null) { showNoFileFoundDialog(); return; } else if (files.length == 0) { showNoFileFoundDialog(); return; } // Opens the most recent image: Arrays.sort(files); String fileName = mediaStorageDir.toString() + File.separator + files[files.length - 1]; mMediaScannerConnection.scanFile(fileName, null); } /** * Starts an intent with the last saved picture as data. This event is then handled by a user * defined app (like the image gallery app). * * @param path Path * @param uri Uri */ @Override public void onScanCompleted(String path, Uri uri) { try { if (uri != null) { Intent intent = new Intent(Intent.ACTION_VIEW); // I do not know why setData(uri) is not working with Marshmallows, it just opens one image (not the folder), with setData(Uri.fromFile) it is working: int currentApiVersion = android.os.Build.VERSION.SDK_INT; if (currentApiVersion >= Build.VERSION_CODES.M) intent.setDataAndType(Uri.fromFile(new File(path)), "image/*"); else intent.setData(uri); // startActivity(intent); } } finally { mMediaScannerConnection.disconnect(); mMediaScannerConnection = null; } } /** * Shows a dialog saying that no saved picture has been found. */ private void showNoFileFoundDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.no_file_found_msg).setTitle(R.string.no_file_found_title); builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }); AlertDialog dialog = builder.create(); dialog.show(); } // ================= end: methods for opening the gallery ================= // ================= start: methods for accessing the location ================= @TargetApi(16) private void requestLocation() { // Check permission if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // ask for permission: ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.ACCESS_FINE_LOCATION }, PERMISSION_ACCESS_FINE_LOCATION); } else { startLocationAccess(); } } private void startLocationAccess() { // This can be used to let the user enable GPS: // Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); // startActivity(intent); LocationHandler.getInstance(this); } // ================= start: methods for saving pictures ================= /** * Callback called after an image has been taken by the camera. */ @SuppressWarnings("deprecation") private void initPictureCallback() { mIsPictureSafe = true; // Callback for picture saving: mPictureCallback = new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { Log.d(TAG, "taking picture"); mTimerCallbacks.onTimerStopped(SHOT_TIME); mTimerCallbacks.onTimerStarted(SHOT_TIME); mTimerCallbacks.onTimerStopped(FLIP_SHOT_TIME); // resume the camera again (this is necessary on the Nexus 5X, but not on the Samsung S5) if (mCameraPreview.getCamera() != null) { mCameraPreview.getCamera().startPreview(); mCameraPreview.startAutoFocus(); } // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // e.printStackTrace(); // } requestPictureSave(data); Log.d(TAG, "took picture"); } }; } /** * Setup a listener for photo shoot button. */ private void setupPhotoShootButtonCallback() { ImageButton photoButton = (ImageButton) findViewById(R.id.photo_button); if (photoButton == null) return; photoButton.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { if (mIsSeriesMode) { mIsSeriesModePaused = !mIsSeriesModePaused; showShootModeToast(); updateMode(); } else if (mIsPictureSafe) { // get an image from the camera takePicture(); } } }); } /** * Initializes the buttons that are used in the camera_controls_layout. These layouts are * recreated on orientation changes, so we need to assign the callbacks again. */ private void initButtons() { setupPhotoShootButtonCallback(); initGalleryCallback(); loadThumbnail(); initShootModeSpinner(); updateMode(); } private void initShootModeSpinner() { // TODO: define the text and the icons in an enum, to ensure that they have the same order. // Spinner for shoot mode: Spinner shootModeSpinner = (Spinner) findViewById(R.id.shoot_mode_spinner); String[] shootModeText = getResources().getStringArray(R.array.shoot_mode_array); Integer[] shootModeIcons = new Integer[] { R.drawable.ic_photo_vector, R.drawable.ic_burst_mode_vector }; shootModeSpinner .setAdapter(new ShootModeAdapter(this, R.layout.spinner_row, shootModeText, shootModeIcons)); shootModeSpinner.setOnItemSelectedListener(this); // Used to prevent firing the onItemSelected method: mItemSelectedAutomatically = true; if (mIsSeriesMode) shootModeSpinner.setSelection(SERIES_POS); } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if ((position == SERIES_POS) && !mIsSeriesMode) { mIsSeriesMode = true; mIsSeriesModePaused = false; // TODO: uncomment: // mCameraPreview.pauseImageProcessing(false); mCameraPreview.startAutoFocus(); } else if (position != SERIES_POS && mIsSeriesMode) { mIsSeriesMode = false; mPaintView.drawMovementIndicator(false); // This is necessary to prevent a drawing of the movement indicator } mCameraPreview.setAwaitFrameChanges(mIsSeriesMode); // Show a toast to the user, but just if he selected the spinner manually: if (!mItemSelectedAutomatically) showShootModeToast(); mItemSelectedAutomatically = false; updateMode(); } private void showShootModeToast() { int msg; if (mIsSeriesMode) { if (mIsSeriesModePaused) msg = R.string.toast_series_paused; else msg = R.string.toast_series_started; } else msg = R.string.toast_single; showToastText(msg); } private void updateShootModeSpinner() { Spinner shootModeSpinner = (Spinner) findViewById(R.id.shoot_mode_spinner); if (mIsSeriesMode) shootModeSpinner.setSelection(SERIES_POS); else shootModeSpinner.setSelection(SINGLE_POS); } private void updateMode() { ImageButton photoButton = (ImageButton) findViewById(R.id.photo_button); if (photoButton == null) return; int drawable; if (mIsSeriesMode) { if (mIsSeriesModePaused) { drawable = R.drawable.ic_play_arrow_24dp; displaySeriesModePaused(); // shows a text in the text view and removes any CVResults shown. } else { drawable = R.drawable.ic_pause_24dp; setTextViewText(R.string.instruction_series_started); } } else { // showToastText(R.string.toast_single); drawable = R.drawable.ic_photo_camera; mIsSeriesModePaused = false; } if (mCameraPreview != null) mCameraPreview.pauseImageProcessing(mIsSeriesModePaused); photoButton.setImageResource(drawable); // TODO: put this into a method used for restoring generic states: if (mIsSeriesMode) getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @Override public void onNothingSelected(AdapterView<?> parent) { } /** * Tells the camera to take a picture. */ private void takePicture() { // if (!mIsPictureSafe) // return; mIsPictureSafe = false; Camera.ShutterCallback shutterCallback = new Camera.ShutterCallback() { @Override public void onShutter() { mPaintView.showFlicker(); } }; mCameraPreview.storeMat(mIsSeriesMode); if (mCameraPreview.getCamera() != null) { mCameraPreview.getCamera().takePicture(shutterCallback, null, mPictureCallback); // if (mCameraPreview.isFrameSteady()) // mCameraPreview.getCamera().takePicture(shutterCallback, null, mPictureCallback); } } private void requestPictureSave(byte[] data) { // Check if we have the permission to save images: if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { mPictureData = data; // ask for permission: ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, PERMISSION_WRITE_EXTERNAL_STORAGE); } else savePicture(data); } /** * Save the image in an own thread (AsyncTask): * @param data image as a byte stream. */ private void savePicture(byte[] data) { Uri uri = getOutputMediaFile(getResources().getString(R.string.app_name)); FileSaver fileSaver = new FileSaver(data); fileSaver.execute(uri); } private void showSaveErrorDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.picture_save_error_text).setTitle(R.string.picture_save_error_title); builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { ChangeDetector.resetNewFrameDetector(); mIsPictureSafe = true; } }); AlertDialog dialog = builder.create(); dialog.show(); } /** * Returns the URI of a new file containing a time stamp. * * @param appName name of the app, this is used for gathering the directory string. * @return the filename. */ private static Uri getOutputMediaFile(String appName) { File mediaStorageDir = getMediaStorageDir(appName); if (mediaStorageDir == null) return null; // Create a media file name mLastTimeStamp = new Date(); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(mLastTimeStamp); File mediaFile = new File(mediaStorageDir.getPath() + File.separator + mContext.getString(R.string.img_prefix) + timeStamp + mContext.getString(R.string.img_extension)); return Uri.fromFile(mediaFile); } /** * Returns the path to the directory in which the images are saved. * * @param appName name of the app, this is used for gathering the directory string. * @return the path where the images are stored. */ public static File getMediaStorageDir(String appName) { File mediaStorageDir = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), appName); // Create the storage directory if it does not exist if (!mediaStorageDir.exists()) { if (!mediaStorageDir.mkdirs()) { return null; } } return mediaStorageDir; } // ================= end: methods for saving pictures ================= // ================= start: methods for navigation drawer ================= @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. if (mDrawerToggle != null) mDrawerToggle.syncState(); } /** * Called after configuration changes -> This includes also orientation change. By handling * orientation changes by ourselves, we can prevent a restart of the camera, which results in a * speedup. * @param newConfig new configuration */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mDrawerToggle.onConfigurationChanged(newConfig); // Tell the camera that the orientation changed, so it can adapt the preview orientation: if (mCameraPreview != null) mCameraPreview.displayRotated(); // Change the layout dynamically: Remove the current camera_controls_layout and add a new // one, which is appropriate for the orientation (portrait or landscape xml's). ViewGroup appRoot = (ViewGroup) findViewById(R.id.main_frame_layout); if (appRoot == null) return; View f = findViewById(R.id.camera_controls_layout); if (f == null) return; appRoot.removeView(f); getLayoutInflater().inflate(R.layout.camera_controls_layout, appRoot); View view = findViewById(R.id.camera_controls_layout); view.setBackgroundColor(getResources().getColor(R.color.control_background_color_transparent)); // Initialize the newly created buttons: initButtons(); } public static Point getPreviewDimension() { // Taken from: http://stackoverflow.com/questions/1016896/get-screen-dimensions-in-pixels View v = ((Activity) mContext).findViewById(R.id.camera_controls_layout); WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); Point size = new Point(); display.getSize(size); Point dim = null; if (v != null) { if (getOrientation() == Surface.ROTATION_0 || getOrientation() == Surface.ROTATION_180) dim = new Point(size.x, size.y - v.getHeight()); // return size.y - v.getHeight(); else if (getOrientation() == Surface.ROTATION_90 || getOrientation() == Surface.ROTATION_270) dim = new Point(size.x - v.getWidth(), size.y); // return size.x - v.getWidth(); } return dim; } private void showDebugView() { // Create the debug view - if it is not already created: if (mDebugViewFragment == null) { mDebugViewFragment = new DebugViewFragment(); } getSupportFragmentManager().beginTransaction() .add(R.id.container_layout, mDebugViewFragment, DEBUG_VIEW_FRAGMENT).commit(); mIsDebugViewEnabled = true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (mDrawerToggle.onOptionsItemSelected(item)) { return true; } switch (item.getItemId()) { case R.id.home: mDrawerLayout.openDrawer(GravityCompat.START); return true; // Show / hide the debug view case R.id.debug_view_item: // Create the debug view - if it is not already created: if (mDebugViewFragment == null) { mDebugViewFragment = new DebugViewFragment(); } // Show the debug view: if (getSupportFragmentManager().findFragmentByTag(DEBUG_VIEW_FRAGMENT) == null) { mIsDebugViewEnabled = true; item.setTitle(R.string.hide_debug_view_text); getSupportFragmentManager().beginTransaction() .add(R.id.container_layout, mDebugViewFragment, DEBUG_VIEW_FRAGMENT).commit(); } // Hide the debug view: else { mIsDebugViewEnabled = false; item.setTitle(R.string.show_debug_view_text); getSupportFragmentManager().beginTransaction().remove(mDebugViewFragment).commit(); } return true; // Switch between the two page segmentation methods: case R.id.use_lab_item: if (NativeWrapper.useLab()) { NativeWrapper.setUseLab(false); item.setTitle(R.string.precise_page_seg_text); } else { NativeWrapper.setUseLab(true); item.setTitle(R.string.fast_page_seg_text); } return true; // Focus measurement: case R.id.show_fm_values_item: if (mPaintView.isFocusTextVisible()) { item.setTitle(R.string.show_fm_values_text); mPaintView.drawFocusText(false); } else { item.setTitle(R.string.hide_fm_values_text); mPaintView.drawFocusText(true); } break; // Guide lines: case R.id.show_guide_item: if (mPaintView.areGuideLinesDrawn()) { mPaintView.drawGuideLines(false); item.setTitle(R.string.show_guide_text); } else { mPaintView.drawGuideLines(true); item.setTitle(R.string.hide_guide_text); } break; // // Threading: // case R.id.threading_item: // if (mCameraPreview.isMultiThreading()) { // mCameraPreview.setThreading(false); // item.setTitle(R.string.multi_thread_text); // } // else { // mCameraPreview.setThreading(true); // item.setTitle(R.string.single_thread_text); // } } return super.onOptionsItemSelected(item); } private void setupToolbar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(false); } } private void setupDebugView() { mDebugViewFragment = (DebugViewFragment) getSupportFragmentManager().findFragmentByTag(DEBUG_VIEW_FRAGMENT); mTimerCallbacks = this; mTextView = (TextView) findViewById(R.id.instruction_view); mIsDebugViewEnabled = (mDebugViewFragment == null); if (mDebugViewFragment == null) mIsDebugViewEnabled = false; else mIsDebugViewEnabled = true; } /** * Initializes the navigation drawer, when the app is started. */ @SuppressWarnings("deprecation") private void setupNavigationDrawer() { mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.drawer_open, R.string.drawer_close) { }; // Set the drawer toggle as the DrawerListener mDrawerLayout.setDrawerListener(mDrawerToggle); NavigationView mDrawer = (NavigationView) findViewById(R.id.left_drawer); setupDrawerContent(mDrawer); // Set the item text for the debug view in the naviation drawer: if (mDrawer == null) return; Menu menu = mDrawer.getMenu(); if (menu == null) return; MenuItem item = menu.findItem(R.id.debug_view_item); if (item == null) return; if (mIsDebugViewEnabled) item.setTitle(R.string.hide_debug_view_text); else item.setTitle(R.string.show_debug_view_text); } /** * Connects the items in the navigation drawer with a listener. * * @param navigationView NavigationView */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem menuItem) { selectDrawerItem(menuItem); return true; } }); } /** * Called after an item is selected in the navigation drawer. * * @param menuItem ID of the selected item. */ private void selectDrawerItem(MenuItem menuItem) { switch (menuItem.getItemId()) { // case R.id.debug_view_item: // // // Create the debug view - if it is not already created: // if (mDebugViewFragment == null) { // mDebugViewFragment = new DebugViewFragment(); // } // // // Show the debug view: // if (getSupportFragmentManager().findFragmentByTag(DEBUG_VIEW_FRAGMENT) == null) { // mIsDebugViewEnabled = true; // menuItem.setTitle(R.string.hide_debug_view_text); // getSupportFragmentManager().beginTransaction().add(R.id.container_layout, mDebugViewFragment, DEBUG_VIEW_FRAGMENT).commit(); // } // // Hide the debug view: // else { // mIsDebugViewEnabled = false; // menuItem.setTitle(R.string.show_debug_view_text); // getSupportFragmentManager().beginTransaction().remove(mDebugViewFragment).commit(); // } // // break; // Focus measurement: case R.id.show_fm_values_item: if (mPaintView.isFocusTextVisible()) { menuItem.setTitle(R.string.show_fm_values_text); mPaintView.drawFocusText(false); } else { menuItem.setTitle(R.string.hide_fm_values_text); mPaintView.drawFocusText(true); } break; // Guide lines: case R.id.show_guide_item: if (mPaintView.areGuideLinesDrawn()) { mPaintView.drawGuideLines(false); menuItem.setTitle(R.string.show_guide_text); } else { mPaintView.drawGuideLines(true); menuItem.setTitle(R.string.hide_guide_text); } break; // // Switch between the two page segmentation methods: // case R.id.action_precise_page_seg: // // if (NativeWrapper.useLab()) { // NativeWrapper.setUseLab(false); // menuItem.setTitle(R.string.precise_page_seg_text); // } // else { // NativeWrapper.setUseLab(true); // menuItem.setTitle(R.string.fast_page_seg_text); // } } mDrawerLayout.closeDrawers(); } // ================= end: methods for navigation drawer ================= // ================= start: CALLBACKS invoking TaskTimer ================= /** * Called before a task is executed. This is used to measure the time of the task execution. * * @param type TaskType of the sender, as defined in TaskTimer. */ @Override public void onTimerStarted(TaskTimer.TaskType type) { // Do nothing if the debug view is not visible: if (!mIsDebugViewEnabled) return; if (mTaskTimer == null) return; mTaskTimer.startTaskTimer(type); } /** * Called after a task is executed. This is used to measure the time of the task execution. * * @param type */ @Override public void onTimerStopped(final TaskTimer.TaskType type) { if (!mIsDebugViewEnabled) return; if (mTaskTimer == null) return; final long timePast = mTaskTimer.getTaskTime(type); // Normally the timer callback should just be called if the debug view is visible: if (mIsDebugViewEnabled) { if (mDebugViewFragment != null) { // The update of the UI elements must be done from the UI thread: runOnUiThread(new Runnable() { @Override public void run() { mDebugViewFragment.setTimeText(type, timePast); } }); } } } // /** // * Returns true if the debug view is visible. Mainly used before TaskTimer events are triggered. // * // * @return boolean // */ // public static boolean isDebugViewEnabled() { // // return mIsDebugViewEnabled; // // } // ================= stop: CALLBACKS invoking TaskTimer ================= public static int getOrientation() { WindowManager w = (WindowManager) mContext.getSystemService(WINDOW_SERVICE); return w.getDefaultDisplay().getRotation(); } // ================= start: CALLBACKS called from native files ================= /** * Called after focus measurement is finished. * * @param patches Patches array */ @Override public void onFocusMeasured(Patch[] patches) { if (mCVResult != null) mCVResult.setPatches(patches); } /** * Called after page segmentation is finished. * * @param dkPolyRects Array of polyRects */ @Override public void onPageSegmented(DkPolyRect[] dkPolyRects, int frameCnt) { // Check if the result is returned from a thread that is already outdated // (an up-to-date result is already computed): if (frameCnt <= mMaxFrameCnt) { Log.d(TAG, "skipped frame id: " + frameCnt); return; } mTimerCallbacks.onTimerStopped(PAGE_SEGMENTATION); long currentTime = System.currentTimeMillis(); long timeDiff = currentTime - mLastTime; Log.d(TAG, "time difference: " + timeDiff); mLastTime = currentTime; if (mCVResult != null) { // mCVResult.setPatches(null); if (!mCVResult.isStable()) mCVResult.setPatches(new Patch[0]); mCVResult.setDKPolyRects(dkPolyRects); } // if (isRectJumping(dkPolyRects)) // mCameraPreview.startFocusMeasurement(false); // else // mCameraPreview.startFocusMeasurement(true); mLastDkPolyRects = dkPolyRects; mMaxFrameCnt = frameCnt; mTimerCallbacks.onTimerStarted(PAGE_SEGMENTATION); } boolean isRectJumping(DkPolyRect[] dkPolyRects) { boolean isJumping = false; Log.d(TAG, "jumping?"); if (dkPolyRects != null && mLastDkPolyRects != null) { Log.d(TAG, "check 1"); if (dkPolyRects.length == 1 && mLastDkPolyRects.length == 1) { Log.d(TAG, "check 2"); PointF distVec = mLastDkPolyRects[0].getLargestDistVector(dkPolyRects[0]); PointF normedPoint = mCVResult.normPoint(distVec); if (normedPoint.length() >= .05) { isJumping = true; } Log.d(TAG, "distance: " + normedPoint.length()); } } return isJumping; } /** * Called after page segmentation is finished. * * @param value illumination value */ @Override public void onIluminationComputed(double value) { if (mCVResult != null) mCVResult.setIllumination(value); } // ================= end: CALLBACKS called from native files ================= // ================= start: CameraPreview.CameraPreviewCallback CALLBACK ================= /** * Called after the dimension of the camera view is set. The dimensions are necessary to convert * the frame coordinates to view coordinates. * * @param width width of the view * @param height height of the view */ @Override public void onMeasuredDimensionChange(int width, int height) { mCVResult.setViewDimensions(width, height); } @Override public void onFlashModesFound(List<String> modes) { mFlashModes = modes; if (mFlashPopupMenu != null) // Menu is not created yet setupFlashUI(); // The flash menu item is not visible at the beginning ('weak' devices might have no flash) if (mFlashModes != null && mFlashMenuItem != null) mFlashMenuItem.setVisible(true); } @Override public void onFocusTouch(PointF point) { Log.d(TAG, "onFocusTouch"); if (mPaintView != null) mPaintView.drawFocusTouch(point); } @Override public void onFocusTouchSuccess() { if (mPaintView != null) mPaintView.drawFocusTouchSuccess(); } @Override public void onMovement(boolean moved) { // This happens if the user has just switched to single mode and the event occurs later than the touch event. // TODO: implement a proper handling of the events. if (!mIsSeriesMode) { mPaintView.drawMovementIndicator(false); return; } else if (mIsSeriesModePaused) { displaySeriesModePaused(); return; } mPaintView.drawMovementIndicator(moved); if (moved) { mCVResult.clearResults(); setTextViewText(R.string.instruction_movement); } else { // This forces an update of the textview if it is still showing the R.string.instruction_movement text if (mTextView.getText() == getResources().getString(R.string.instruction_movement)) setTextViewText(R.string.instruction_none); } } @Override public void onWaitingForDoc(boolean waiting) { if (mIsSeriesModePaused) { displaySeriesModePaused(); return; } if (waiting) setTextViewText(R.string.instruction_no_changes); } @Override public void onCaptureVerified() { if (!mIsPictureSafe) return; // Tell the user that a picture will be taken: runOnUiThread(new Runnable() { @Override public void run() { // This code will always run on the UI thread, therefore is safe to modify UI elements. mTextView.setText(getResources().getString(R.string.taking_picture_text)); } }); takePicture(); } private void setTextViewText(int msg) { final String msgText = getResources().getString(msg); runOnUiThread(new Runnable() { @Override public void run() { mTextView.setText(msgText); } }); } public void showFlashPopup(MenuItem item) { View menuItemView = findViewById(R.id.flash_mode_item); if (menuItemView == null) return; // Create the menu for the first time: if (mFlashPopupMenu == null) { mFlashPopupMenu = new PopupMenu(this, menuItemView); mFlashPopupMenu.setOnMenuItemClickListener(this); mFlashPopupMenu.inflate(R.menu.flash_mode_menu); setupFlashUI(); } mFlashPopupMenu.show(); } @SuppressWarnings("deprecation") private void setupFlashUI() { if (!mFlashModes.contains(Camera.Parameters.FLASH_MODE_AUTO)) mFlashPopupMenu.getMenu().findItem(R.id.flash_auto_item).setVisible(false); if (!mFlashModes.contains(Camera.Parameters.FLASH_MODE_ON)) mFlashPopupMenu.getMenu().findItem(R.id.flash_on_item).setVisible(false); if (!mFlashModes.contains(Camera.Parameters.FLASH_MODE_OFF)) mFlashPopupMenu.getMenu().findItem(R.id.flash_off_item).setVisible(false); if (!mFlashModes.contains(Camera.Parameters.FLASH_MODE_TORCH)) mFlashPopupMenu.getMenu().findItem(R.id.flash_torch_item).setVisible(false); } @Override @SuppressWarnings("deprecation") public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.flash_auto_item: mFlashMenuItem.setIcon(mFlashAutoDrawable); mCameraPreview.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO); return true; case R.id.flash_off_item: mFlashMenuItem.setIcon(mFlashOffDrawable); mCameraPreview.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); // mFlashMode = Camera.Parameters.FLASH_MODE_OFF; return true; case R.id.flash_on_item: mFlashMenuItem.setIcon(mFlashOnDrawable); mCameraPreview.setFlashMode(Camera.Parameters.FLASH_MODE_ON); // mFlashMode = Camera.Parameters.FLASH_MODE_ON; return true; case R.id.flash_torch_item: mFlashMenuItem.setIcon(mFlashTorchDrawable); mCameraPreview.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); // mFlashMode = Camera.Parameters.FLASH_MODE_ON; return true; default: return false; } } /** * Called after the dimension of the camera frame is set. The dimensions are necessary to convert * the frame coordinates to view coordinates. * * @param width width of the frame * @param height height of the frame * @param cameraOrientation orientation of the camera */ @Override public void onFrameDimensionChange(int width, int height, int cameraOrientation) { mCameraOrientation = cameraOrientation; mCVResult.setFrameDimensions(width, height, cameraOrientation); CameraPaintLayout l = (CameraPaintLayout) findViewById(R.id.camera_paint_layout); if (l != null) l.setFrameDimensions(width, height); View v = findViewById(R.id.camera_controls_layout); if ((v != null) && (!mCameraPreview.isPreviewFitting())) v.setBackgroundColor(getResources().getColor(R.color.control_background_color_transparent)); } /** * Called after the the status of the CVResult object is changed. * @param state state of the CVResult */ @Override public void onStatusChange(final int state) { if (mIsSeriesModePaused) { displaySeriesModePaused(); return; } // Check if we need the focus measurement at this point: if (state == CVResult.DOCUMENT_STATE_NO_FOCUS_MEASURED) { // if (mCVResult.isStable()) mCameraPreview.startFocusMeasurement(true); // else // mCameraPreview.startFocusMeasurement(false); } // TODO: I do not know why this is happening, once the CameraActivity is resumed: else if (state > CVResult.DOCUMENT_STATE_NO_FOCUS_MEASURED && !mCameraPreview.isFocusMeasured()) { // if (mCVResult.isStable()) mCameraPreview.startFocusMeasurement(true); // else // mCameraPreview.startFocusMeasurement(false); } else if (state < CVResult.DOCUMENT_STATE_NO_FOCUS_MEASURED) { mCameraPreview.startFocusMeasurement(false); } final String msg; if (!mIsPictureSafe) { msg = getResources().getString(R.string.taking_picture_text); } else if (!mIsSeriesMode || state != CVResult.DOCUMENT_STATE_OK) { msg = getInstructionMessage(state); } else { if (mIsSeriesMode) { mCameraPreview.verifyCapture(); return; } else { msg = getResources().getString(R.string.taking_picture_text); if (mIsPictureSafe) takePicture(); } } runOnUiThread(new Runnable() { @Override public void run() { // This code will always run on the UI thread, therefore is safe to modify UI elements. mTextView.setText(msg); } }); } private void displaySeriesModePaused() { if (mCVResult != null) mCVResult.clearResults(); if (mPaintView != null) mPaintView.clearScreen(); setTextViewText(R.string.instruction_series_paused); } private void showToastText(int id) { String msg = getResources().getString(id); if (mToast != null) mToast.cancel(); mToast = Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT); mToast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0); mToast.show(); } /** * Returns instruction messages, depending on the current state of the CVResult object. * @param state state of the CVResult object * @return instruction message */ private String getInstructionMessage(int state) { switch (state) { case CVResult.DOCUMENT_STATE_EMPTY: return getResources().getString(R.string.instruction_empty); case CVResult.DOCUMENT_STATE_OK: return getResources().getString(R.string.instruction_ok); case CVResult.DOCUMENT_STATE_SMALL: return getResources().getString(R.string.instruction_small); case CVResult.DOCUMENT_STATE_PERSPECTIVE: return getResources().getString(R.string.instruction_perspective); case CVResult.DOCUMENT_STATE_UNSHARP: return getResources().getString(R.string.instruction_unsharp); case CVResult.DOCUMENT_STATE_BAD_ILLUMINATION: return getResources().getString(R.string.instruction_bad_illumination); case CVResult.DOCUMENT_STATE_ROTATION: return getResources().getString(R.string.instruction_rotation); case CVResult.DOCUMENT_STATE_NO_FOCUS_MEASURED: return getResources().getString(R.string.instruction_no_focus_measured); case CVResult.DOCUMENT_STATE_NO_ILLUMINATION_MEASURED: return getResources().getString(R.string.instruction_no_illumination_measured); } return getResources().getString(R.string.instruction_unknown); } // ================= end: CameraPreview.DimensionChange CALLBACK ================= /** * Shows the last picture taken as a thumbnail on the gallery button. */ private void loadThumbnail() { // Check if a thumbnail is already existing (this should occur on orientation changes): if (mGalleryButtonDrawable != null) setGalleryButtonDrawable(mGalleryButtonDrawable); // Load the most recent image from the folder: else { File mediaStorageDir = getMediaStorageDir(getResources().getString(R.string.app_name)); if (mediaStorageDir == null) return; String[] files = mediaStorageDir.list(); if (files == null) return; else if (files.length == 0) return; // Determine the most recent image: Arrays.sort(files); String fileName = mediaStorageDir.toString() + "/" + files[files.length - 1]; ThumbnailLoader thumbnailLoader = new ThumbnailLoader(); thumbnailLoader.execute(fileName); } } @SuppressWarnings("deprecation") private void setGalleryButtonDrawable(Drawable drawable) { mGalleryButton.setVisibility(View.VISIBLE); // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) // mGalleryButton.setBackground(drawable); // else // mGalleryButton.setBackgroundDrawable(drawable); // mGalleryButton.setImageDrawable(drawable); // mGalleryButton.setAdjustViewBounds(true); // mGalleryButton.setScaleType(ImageView.ScaleType.CENTER_CROP); // Keep a reference to the drawable, because we need it if the orientation is changed: mGalleryButtonDrawable = drawable; } private int getExifOrientation() { switch (mCameraOrientation) { case 0: return 1; case 90: return 6; case 180: return 3; case 270: return 8; } return -1; } private int getAngleFromExif(int orientation) { switch (orientation) { case 1: return 0; case 6: return 90; case 3: return 180; case 8: return 270; } return -1; } @Override protected NavigationDrawer.NavigationItemEnum getSelfNavDrawerItem() { return NavigationDrawer.NavigationItemEnum.CAMERA; } /** * Class responsible for loading thumbnails from images. This is time intense and hence it is * done in an own thread (AsyncTask). */ private class ThumbnailLoader extends AsyncTask<String, Void, Void> { @Override protected Void doInBackground(String... fileNames) { String fileName = fileNames[0]; Bitmap thumbNailBitmap = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(fileName), 200, 200); if (thumbNailBitmap == null) return null; // Determine the rotation angle of the image: int angle = -1; try { ExifInterface exif = new ExifInterface(fileName); String attr = exif.getAttribute(ExifInterface.TAG_ORIENTATION); angle = getAngleFromExif(Integer.valueOf(attr)); } catch (IOException e) { return null; } //Rotate the image: Matrix mtx = new Matrix(); mtx.setRotate(angle); thumbNailBitmap = Bitmap.createBitmap(thumbNailBitmap, 0, 0, thumbNailBitmap.getWidth(), thumbNailBitmap.getHeight(), mtx, true); // Update the gallery button: final BitmapDrawable thumbDrawable = new BitmapDrawable(getResources(), thumbNailBitmap); if (thumbDrawable == null) return null; runOnUiThread(new Runnable() { @Override public void run() { setGalleryButtonDrawable(thumbDrawable); } }); return null; } } /** * Class used to save pictures in an own thread (AsyncTask). */ private class FileSaver extends AsyncTask<Uri, Void, Void> { private byte[] mData; public FileSaver(byte[] data) { mData = data; } @Override protected Void doInBackground(Uri... uris) { mIsSaving = true; final File outFile = new File(uris[0].getPath()); if (outFile == null) return null; try { runOnUiThread(new Runnable() { @Override public void run() { mGalleryButton.setVisibility(View.INVISIBLE); mProgressBar.setVisibility(View.VISIBLE); } }); FileOutputStream fos = new FileOutputStream(outFile); fos.write(mData); fos.close(); // Save exif information (especially the orientation): saveExif(outFile); // Set the thumbnail on the gallery button, this must be done one the UI thread: updateThumbnail(outFile); // Add the file to the sync list: addToSyncList(outFile); mIsPictureSafe = true; } catch (Exception e) { runOnUiThread(new Runnable() { @Override public void run() { mProgressBar.setVisibility(View.INVISIBLE); mGalleryButton.setVisibility(View.VISIBLE); mIsPictureSafe = false; showSaveErrorDialog(); } }); // Log.d(TAG, "Could not save file: " + outFile); } return null; } private void updateThumbnail(final File outFile) { MediaScannerConnection.scanFile(getApplicationContext(), new String[] { outFile.toString() }, null, new MediaScannerConnection.OnScanCompletedListener() { public void onScanCompleted(String path, Uri uri) { // Before ThumbnailUtils.extractThumbnail was used which causes OOM's: // Bitmap resized = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(outFile.toString()), 200, 200); // Instead use this method to avoid loading large images into memory: Bitmap resized = decodeFile(outFile, 200, 200); Matrix mtx = new Matrix(); mtx.setRotate(mCameraOrientation); resized = Bitmap.createBitmap(resized, 0, 0, resized.getWidth(), resized.getHeight(), mtx, true); final BitmapDrawable thumbDrawable = new BitmapDrawable(getResources(), resized); runOnUiThread(new Runnable() { @Override public void run() { mProgressBar.setVisibility(View.INVISIBLE); setGalleryButtonDrawable(thumbDrawable); } }); } // } }); } private void addToSyncList(File outFile) { SyncInfo.getInstance().addFile(outFile); } private void saveExif(File outFile) throws IOException { final ExifInterface exif = new ExifInterface(outFile.getAbsolutePath()); if (exif != null) { // Save the orientation of the image: int orientation = getExifOrientation(); String exifOrientation = Integer.toString(orientation); exif.setAttribute(ExifInterface.TAG_ORIENTATION, exifOrientation); // Save the GPS coordinates if available: Location location = LocationHandler.getInstance(mContext).getLocation(); if (location != null) { double latitude = location.getLatitude(); double longitude = location.getLongitude(); // Taken from http://stackoverflow.com/questions/5280479/how-to-save-gps-coordinates-in-exif-data-on-android (post by fabien): exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, GPS.convert(latitude)); exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, GPS.latitudeRef(latitude)); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, GPS.convert(longitude)); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, GPS.longitudeRef(longitude)); } // // Log the shot: if (AppState.isDataLogged()) { GPS gps = new GPS(location); DataLog.getInstance().logShot(outFile.getAbsolutePath(), gps, mLastTimeStamp, mIsSeriesMode); } exif.saveAttributes(); } } protected void onPostExecute(Void v) { mIsSaving = false; // Release the memory. Note this is essential, because otherwise allocated memory will increase. mData = null; } } private static Bitmap decodeFile(File f, int width, int height) { try { //Decode image size BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(new FileInputStream(f), null, options); if (options.outWidth > options.outHeight) height = (int) (height * (float) width / options.outWidth); else width = (int) (width * (float) height / options.outHeight); options.inSampleSize = calculateInSampleSize(options, width, height); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeStream(new FileInputStream(f), null, options); } catch (FileNotFoundException e) { } return null; } private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } return inSampleSize; } }