Java tutorial
package edu.mit.mobile.android.livingpostcards; /* * Copyright (C) 2012-2013 MIT Mobile Experience Lab * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation version 2 * of the License. * * This program 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 General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import android.content.ContentValues; import android.content.Intent; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.hardware.Camera; import android.hardware.Camera.AutoFocusCallback; import android.hardware.Camera.Parameters; import android.hardware.Camera.PictureCallback; import android.hardware.Camera.Size; import android.location.Location; import android.location.LocationListener; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.Window; import android.view.WindowManager; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.actionbarsherlock.ActionBarSherlock; import edu.mit.mobile.android.flipr.R; import edu.mit.mobile.android.imagecache.ImageCache; import edu.mit.mobile.android.imagecache.ImageCache.OnImageLoadListener; import edu.mit.mobile.android.livingpostcards.CameraPreview.OnPreviewStartedListener; import edu.mit.mobile.android.livingpostcards.auth.Authenticator; import edu.mit.mobile.android.livingpostcards.data.Card; import edu.mit.mobile.android.livingpostcards.data.CardMedia; import edu.mit.mobile.android.locast.Constants; import edu.mit.mobile.android.locast.data.CastMedia.CastMediaInfo; import edu.mit.mobile.android.locast.data.MediaProcessingException; import edu.mit.mobile.android.location.IncrementalLocator; import edu.mit.mobile.android.maps.GeocodeTask; import edu.mit.mobile.android.widget.MultiLevelButton; import edu.mit.mobile.android.widget.MultiLevelButton.OnChangeLevelListener; public class CameraActivity extends FragmentActivity implements OnClickListener, OnImageLoadListener, OnCheckedChangeListener, LoaderCallbacks<Cursor>, OnTouchListener { private static final String TAG = CameraActivity.class.getSimpleName(); public static final String ACTION_ADD_PHOTO = "edu.mit.mobile.android.ACTION_ADD_PHOTO"; private final ActionBarSherlock mSherlock = ActionBarSherlock.wrap(this); private Camera mCamera; private CameraPreview mPreview; private FrameLayout mPreviewHolder; private ImageCache mImageCache; private Uri mCard; private Uri mCardDir; private ImageView mOnionSkin; private View mCaptureButton; private MultiLevelButton mOnionskinToggle; private IncrementalLocator mLocator; protected Location mLocation; private Uri mRecentImage; private static final int LOADER_CARD = 100, LOADER_CARDMEDIA = 101; private static final String[] CARD_MEDIA_PROJECTION = new String[] { CardMedia._ID, CardMedia.COL_LOCAL_URL, CardMedia.COL_MEDIA_URL }; private static final int MSG_RELOAD_CARD_AND_MEDIA = 100; private static final int MSG_START_AUTOFOCUS = 101; private static final String INSTANCE_CARD = "edu.mit.mobile.android.livingpostcards.INSTANCE_CARD"; private static class MyHandler extends Handler { private final CameraActivity mActivity; public MyHandler(CameraActivity activity) { mActivity = activity; } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_RELOAD_CARD_AND_MEDIA: final LoaderManager lm = mActivity.getSupportLoaderManager(); lm.restartLoader(LOADER_CARD, null, mActivity); lm.restartLoader(LOADER_CARDMEDIA, null, mActivity); break; case MSG_START_AUTOFOCUS: mActivity.onShutterHalfwayPressed(); break; } } } private final Handler mHandler = new MyHandler(this); private final OnChangeLevelListener mOnionskinChangeLevel = new OnChangeLevelListener() { @Override public int onChangeLevel(MultiLevelButton b, int curLevel) { int newLevel; switch (curLevel) { case 0: newLevel = 25; break; case 25: newLevel = 50; break; case 50: newLevel = 75; break; default: newLevel = 0; break; } setOnionSkinVisible(newLevel); return newLevel; } }; private final OnPreviewStartedListener mOnPreviewStartedListener = new OnPreviewStartedListener() { @Override public void onPreviewStarted() { Log.d(TAG, "onPreviewStarted"); autoFocus(); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSherlock.requestFeature(Window.FEATURE_NO_TITLE); mSherlock.setContentView(R.layout.activity_camera); mPreviewHolder = (FrameLayout) findViewById(R.id.camera_preview); mOnionSkin = (ImageView) findViewById(R.id.onion_skin_image); mCaptureButton = findViewById(R.id.capture); mCaptureButton.setOnClickListener(this); mCaptureButton.setOnTouchListener(this); mOnionskinToggle = (MultiLevelButton) findViewById(R.id.onion_skin_toggle); mOnionskinToggle.setOnChangeLevelListener(mOnionskinChangeLevel); ((CompoundButton) findViewById(R.id.grid_toggle)).setOnCheckedChangeListener(this); findViewById(R.id.done).setOnClickListener(this); setFullscreen(true); mLocator = new IncrementalLocator(this); mImageCache = ImageCache.getInstance(this); processIntent(getIntent()); if (savedInstanceState != null) { final Uri card = savedInstanceState.getParcelable(INSTANCE_CARD); loadCard(card); } } private void processIntent(Intent intent) { final String action = intent.getAction(); if (ACTION_ADD_PHOTO.equals(action)) { mCardDir = null; loadCard(intent.getData()); } else if (Intent.ACTION_INSERT.equals(action)) { mCard = null; mCardDir = intent.getData(); } else { Toast.makeText(this, "Unable to handle requested action", Toast.LENGTH_LONG).show(); setResult(RESULT_CANCELED); finish(); } } private void loadCard(Uri card) { mCard = card; mHandler.sendEmptyMessage(MSG_RELOAD_CARD_AND_MEDIA); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(INSTANCE_CARD, mCard); } @Override protected void onPause() { super.onPause(); mLocator.removeLocationUpdates(mLocationListener); mImageCache.unregisterOnImageLoadListener(this); mPreview.setOnPreviewStartedListener(null); if (mCamera != null) { mCamera.stopPreview(); } mPreviewHolder.removeAllViews(); mPreview = null; releaseCamera(); } @Override protected void onResume() { super.onResume(); mLocator.requestLocationUpdates(mLocationListener); mImageCache.registerOnImageLoadListener(this); mCamera = getCameraInstance(); if (mCamera != null) { mPreview = new CameraPreview(this, mCamera); mPreview.setForceAspectRatio((float) 640 / 480); mPreviewHolder.addView(mPreview); mPreview.setOnPreviewStartedListener(mOnPreviewStartedListener); } else { Toast.makeText(this, R.string.err_initializing_camera, Toast.LENGTH_LONG).show(); setResult(RESULT_CANCELED); finish(); } setOnionSkinVisible(mOnionskinToggle.getLevel()); } public void setFullscreen(boolean fullscreen) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { mPreviewHolder.setSystemUiVisibility(fullscreen ? View.SYSTEM_UI_FLAG_LOW_PROFILE : 0); } if (fullscreen) { final Window w = getWindow(); w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); w.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { final Window w = getWindow(); w.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); w.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } /** * Loads the given image in the onion skin. This requests the image cache to load it, so it * returns immediately. * * @param image */ private void showOnionskinImage(Uri image) { try { final Drawable d = mImageCache.loadImage(R.id.camera_preview, image, 640, 480); if (d != null) { loadOnionskinImage(d); } } catch (final IOException e) { e.printStackTrace(); } } private void setOnionSkinVisible(int level) { mOnionSkin.setVisibility(level > 0 ? View.VISIBLE : View.GONE); setOnionskinAlphaPercent(level); } private void invalidateOnionskinImage() { mOnionSkin.setImageDrawable(null); mOnionskinToggle.setEnabled(false); } @SuppressWarnings("deprecation") private void setOnionskinAlphaPercent(int percent) { mOnionSkin.setAlpha((int) (percent / 100.0 * 255)); } private void loadOnionskinImage(Drawable image) { mOnionSkin.setImageDrawable(image); setOnionskinAlphaPercent(mOnionskinToggle.getLevel()); mOnionskinToggle.setEnabled(true); } @Override public void onImageLoaded(final int id, Uri imageUri, Drawable image) { if (R.id.camera_preview == id) { loadOnionskinImage(image); } } // //////////////////////////////////////////////////////////// // /// camera // /////////////////////////////////////////////////////////// private void releaseCamera() { if (mCamera != null) { mCamera.release(); // release the camera for other applications mCamera = null; } } /** A safe way to get an instance of the Camera object. */ public static Camera getCameraInstance() { Camera c = null; try { c = Camera.open(); // attempt to get a Camera instance final Parameters params = c.getParameters(); final Size s = getBestPictureSize(640, 480, params); params.setPictureSize(s.width, s.height); if (Constants.DEBUG) { Log.d(TAG, "best picture size is " + s.width + "x" + s.height); } c.setParameters(params); } catch (final Exception e) { Log.e(TAG, "Error acquiring camera", e); } return c; // returns null if camera is unavailable } private volatile boolean mDelayedCapture; private void capture() { mCaptureButton.setEnabled(false); if (mAutofocusStarted) { // don't capture while autofocusing. Capture will be done on the callback. mDelayedCapture = true; return; } invalidateOnionskinImage(); try { mCamera.takePicture(null, null, mPictureCallback); // make this error non-fatal. } catch (final RuntimeException re) { Toast.makeText(CameraActivity.this, R.string.err_camera_take_picture_failed, Toast.LENGTH_LONG).show(); Log.e(TAG, "Error taking picture", re); setReadyToCapture(); } } private final PictureCallback mPictureCallback = new PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { mCamera.cancelAutoFocus(); mCamera.startPreview(); setFullscreen(true); savePicture(data); } }; /** * Saves the picture as a jpeg to disk and adds it as a media item. This method starts a task * and returns immediately. * * @param data */ private void savePicture(byte[] data) { new SavePictureTask().execute(data); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.capture: capture(); break; case R.id.done: setResult(RESULT_OK); finish(); // when a new card is added, show the editor immediately afterward. if (mCard != null && Intent.ACTION_INSERT.equals(getIntent().getAction())) { startActivity(new Intent(Intent.ACTION_EDIT, mCard)); } break; } } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { switch (buttonView.getId()) { case R.id.grid_toggle: findViewById(R.id.grid).setVisibility(isChecked ? View.VISIBLE : View.GONE); } } @Override public boolean onTouch(View v, MotionEvent event) { switch (v.getId()) { case R.id.capture: switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mHandler.sendEmptyMessageDelayed(MSG_START_AUTOFOCUS, 500); break; case MotionEvent.ACTION_UP: mHandler.removeMessages(MSG_START_AUTOFOCUS); break; } return false; default: return false; } } private volatile boolean mAutofocusStarted = false; /** * Called when the shutter button has been pressed and held halfway. */ private void onShutterHalfwayPressed() { autoFocus(); } private synchronized void autoFocus() { if (!mAutofocusStarted) { mAutofocusStarted = true; mCamera.autoFocus(mAutoFocusCallback); } } AutoFocusCallback mAutoFocusCallback = new AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { mAutofocusStarted = false; if (mDelayedCapture) { if (success) { capture(); } else { setReadyToCapture(); } mDelayedCapture = false; } } }; // ///////////////////////////////////////////////////////////////////// // content loading // ///////////////////////////////////////////////////////////////////// @Override public Loader<Cursor> onCreateLoader(int loader, Bundle args) { switch (loader) { case LOADER_CARD: return new CursorLoader(this, mCard, null, null, null, null); case LOADER_CARDMEDIA: return new CursorLoader(this, Card.MEDIA.getUri(mCard), CARD_MEDIA_PROJECTION, null, null, CardMedia._ID + " DESC LIMIT 1"); default: return null; } } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { switch (loader.getId()) { case LOADER_CARD: if (c.moveToFirst()) { setTitle(Card.getTitle(this, c)); } break; case LOADER_CARDMEDIA: showLastPhoto(c); break; } } @Override public void setTitle(CharSequence title) { super.setTitle(title); ((TextView) findViewById(R.id.title)).setText(title); } /** * Given a card media cursor, load the most recent photo. This assumes that the cursor was * queried such that the most recent item is last in the cursor (the default sort does this). * * @param cardMedia */ private void showLastPhoto(Cursor cardMedia) { if (cardMedia.moveToLast()) { String localUrl = cardMedia.getString(cardMedia.getColumnIndex(CardMedia.COL_LOCAL_URL)); if (localUrl == null) { localUrl = cardMedia.getString(cardMedia.getColumnIndexOrThrow(CardMedia.COL_MEDIA_URL)); } if (localUrl != null) { showOnionskinImage(Uri.parse(localUrl)); } } } @Override public void onLoaderReset(Loader<Cursor> arg0) { invalidateOnionskinImage(); } private void createNewCard() { final ContentValues cv = new ContentValues(); cv.put(Card.COL_TITLE, ""); if (mRecentImage != null) { cv.put(Card.COL_THUMBNAIL, mRecentImage.toString()); } if (mLocation != null) { cv.put(Card.COL_LATITUDE, mLocation.getLatitude()); cv.put(Card.COL_LONGITUDE, mLocation.getLongitude()); } final Uri card = Card.createNewCard(this, Authenticator.getFirstAccount(this, Authenticator.ACCOUNT_TYPE), cv); final Intent intent = new Intent(CameraActivity.ACTION_ADD_PHOTO, card); processIntent(intent); } private final LocationListener mLocationListener = new LocationListener() { @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { // TODO Auto-generated method stub } @Override public void onProviderDisabled(String provider) { // TODO Auto-generated method stub } @Override public void onLocationChanged(Location location) { mLocation = location; showLocationAsText(location); } }; /** * Saves the given jpeg bytes to disk and adds an entry to the CardMedia list. Pictures are * stored to external storage under {@link StorageUtils#EXTERNAL_PICTURE_SUBDIR}. * */ private class SavePictureTask extends AsyncTask<byte[], Long, Uri> { private Exception mErr; @Override protected void onPreExecute() { CameraActivity.this.setProgressBarIndeterminateVisibility(true); super.onPreExecute(); } @Override protected Uri doInBackground(byte[]... data) { if (data == null || data.length == 0 || data[0] == null || data[0].length == 0) { mErr = new IllegalArgumentException("data was null or empty"); return null; } final File externalPicturesDir = StorageUtils.getExternalPictureDir(CameraActivity.this); if (externalPicturesDir == null) { mErr = new RuntimeException("no external storage available"); return null; } final String timeStamp = new SimpleDateFormat("yyyy-MM-dd_HHmmss.SSSZ", Locale.US).format(new Date()); final File outFile = new File(externalPicturesDir, "IMG_" + timeStamp + ".jpg"); externalPicturesDir.mkdirs(); try { final FileOutputStream fos = new FileOutputStream(outFile); fos.write(data[0]); fos.close(); final Uri mediaUri = Uri.fromFile(outFile); mRecentImage = mediaUri; if (mCard == null && mCardDir != null) { createNewCard(); } mImageCache.scheduleLoadImage(0, mediaUri, 640, 480); final CastMediaInfo cmi = CardMedia.addMediaToCard(CameraActivity.this, Authenticator.getFirstAccount(CameraActivity.this), Card.MEDIA.getUri(mCard), mediaUri); return cmi.castMediaItem; } catch (final IOException e) { mErr = e; return null; } catch (final RuntimeException re) { mErr = re; return null; } catch (final MediaProcessingException e) { mErr = e; return null; } } @Override protected void onPostExecute(Uri result) { if (mErr != null) { Log.e(TAG, "error writing file", mErr); Toast.makeText(CameraActivity.this, R.string.err_camera_take_picture_failed, Toast.LENGTH_LONG) .show(); } setReadyToCapture(); } } private void setReadyToCapture() { CameraActivity.this.setProgressBarIndeterminateVisibility(false); if (mCamera != null) { mCaptureButton.setEnabled(true); mCamera.startPreview(); } else { mCaptureButton.setEnabled(false); } } private GeocodeTask mGeocodeTask; /** * Displays the given location as text by reverse geocoding it. The result is displayed * asynchronously. * * @param location */ protected void showLocationAsText(Location location) { if (mGeocodeTask != null) { mGeocodeTask.cancel(true); } mGeocodeTask = new GeocodeTask(this, (TextView) findViewById(R.id.location)); mGeocodeTask.execute(location); } /*** * Copyright (c) 2008-2012 CommonsWare, LLC Licensed under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. You may obtain a * copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required by * applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and limitations under the * License. From _The Busy Coder's Guide to Advanced Android Development_ * http://commonsware.com/AdvAndroid */ /** * Finds the highest resolution picture size that fits within the given width and height. * * @param width * @param height * @param parameters * @return */ public static Camera.Size getBestPictureSize(int width, int height, Camera.Parameters parameters) { Camera.Size result = null; for (final Camera.Size size : parameters.getSupportedPictureSizes()) { if (size.width <= width && size.height <= height) { if (result == null) { result = size; } else { final int resultArea = result.width * result.height; final int newArea = size.width * size.height; if (newArea > resultArea) { result = size; } } } } return result; } @Override public void onImageLoaded(long id, Uri imageUri, Drawable image) { // TODO Auto-generated method stub } }