在做用户的头像时,忽然想到前段时间(可能是很久以前了),支付宝传出偷偷拍摄用户的生活照,真实头像,被喷的很厉害。然而作为Android开发者的我第一反应竟然是握草,他是怎么实现的。在我印象中,iOS对权限的控制是很严格的,偷偷调起摄像头这种行为应该是很困难的。然而Android4.2之前可以说开发者几乎拥有了系统权限,能力之强简直可怕。而现在Android已经到了7.0,虽然大多说用户还是在4.4到6.0的。我想我也来做一个静默拍摄的app。
正文:
所谓静默拍摄就是在用户毫无感知的情况下拍摄。
一般的拍照都会有预览区域,拍照声。去掉这些东西才算是真正意义上的静默拍摄。
首先,做了一个非常正常的自拍软件,就一个按钮。拍完之后存到文件夹的一个位置。然后我试了一下,完全ok并没有什么难度。然后就是清空surfaceView了。我首先想到的就是setVisiblity为gone,然后就报错了。很尴尬。下一个方案就是用高度和宽度都是0的方法,然而并没有什么卵用,更加尴尬。
然后想想没有有什么好办法了那就把这个surfaceView盖住好了,非常完美,随便搞一搞就盖住了,然后照片照样拍。合理。
但是“咔嚓”一声的拍照声实在令人尴尬,然后我就想到了静音,在页面打开的时候就设置静音。看上去这是一个非常稳健的方法,然后就发生了更加尴尬的事情。设置静音的时候,手机振动了一下,震一下也就算了,关键是还没有把拍照的声音去除。然后我就去查了查了相机音量应该是哪个。之后悲催的事情就发生了:
Google的Android开发者为了Android用户的用户体验,也为了避免开发者开发出静默拍摄的app从而侵犯了隐私,他们就把快门声音的播放函数写在了拍照的方法里面,还是写在framework层的。瞬间我就很难过了。作为一个平凡的第三方开发者,我并没有那么多权限去改变framework层的方法。
然后智慧的我决定曲线救国。因为在预览的时候,并没有进行拍照,但实际上我们已经拿到了相机带来的图片流。这很关键。然后我就把这个图片流变成了bitmap,然后保存到了本地,接着就把相机关了。神不知鬼不觉地把自拍拿到了。当然其中有一点小问题,比如图片编码,图片旋转,本地存储,获取帧图像都是各种各样的问题。但这些都是可以解决的。思路依旧是我上面提到的思路,各种表现方式可以由大家自己搞。
代码如下 | 复制代码 |
publicclassMainActivityextendsAppCompatActivity { staticfinalString TAG ="CAMERA ACTIVITY";
//Camera object Camera mCamera; //Preview surface SurfaceView surfaceView; //Preview surface handle for callback SurfaceHolder surfaceHolder; //Camera button Button btnCapture; //Note if preview windows is on. booleanpreviewing;
intmCurrentCamIndex =0; privateAudioManager manager; privateintvolumn; privatebooleancanTake=false; privateImageView imageView;
@Override protectedvoidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
btnCapture = (Button) findViewById(R.id.btn_capture); imageView =(ImageView)findViewById(R.id.iv); btnCapture.setOnClickListener(newButton.OnClickListener() { publicvoidonClick(View arg0) { canTake=true; } });
surfaceView = (SurfaceView) findViewById(R.id.surfaceView1); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(newSurfaceViewCallback()); //surfaceHolder.addCallback(this); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
publicvoidgetSurfacePic(byte[] data, Camera camera,String name){ Camera.Size size = camera.getParameters().getPreviewSize(); YuvImage image =newYuvImage(data, ImageFormat.NV21, size.width, size.height,null); if(image!=null){ ByteArrayOutputStream stream =newByteArrayOutputStream(); image.compressToJpeg(newRect(0,0, size.width, size.height),80, stream);
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(),0, stream.size());
//********************** //因为图片会放生旋转,因此要对图片进行旋转到和手机在一个方向上 rotateMyBitmap(bmp,name); //**********************************
} }
/** 保存方法 */ publicvoidsaveBitmap(Bitmap bm,String name) { Log.e(TAG,"保存图片"); File f =newFile("/sdcard/namecard/", name); if(f.exists()) { f.delete(); } try{ FileOutputStream out =newFileOutputStream(f); bm.compress(Bitmap.CompressFormat.PNG,90, out); out.flush(); out.close(); Log.e(TAG,"已经保存"); }catch(FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); }catch(IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
}
/** * 保存图片到指定文件夹 * * @param bmp * @return */ privatebooleansaveBitmapTofile(byte[] bmp) { String fileName = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) .toString() + File.separator +"PicTest_"+ System.currentTimeMillis() +".jpg"; File file =newFile(fileName); if(!file.getParentFile().exists()) { file.getParentFile().mkdir(); }
try{ BufferedOutputStream bos =newBufferedOutputStream( newFileOutputStream(file)); bos.write(bmp); bos.flush(); bos.close(); scanFileToPhotoAlbum(file.getAbsolutePath()); Toast.makeText(MainActivity.this,"[Test] Photo take and store in"+ file.toString(),Toast.LENGTH_LONG).show(); }catch(Exception e) { Toast.makeText(MainActivity.this,"Picture Failed"+ e.toString(), Toast.LENGTH_LONG).show(); } returntrue; }
publicvoidsaveMyBitmap(Bitmap mBitmap,String bitName) { String fileName = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) .toString() + File.separator +"PicTest_"+ System.currentTimeMillis() +".jpg"; File file =newFile(fileName); if(!file.getParentFile().exists()) { file.getParentFile().mkdir(); } FileOutputStream fOut =null; try{ fOut =newFileOutputStream(file); }catch(FileNotFoundException e) { e.printStackTrace(); }
try{ if(null!= fOut) { mBitmap.compress(Bitmap.CompressFormat.JPEG,100, fOut); fOut.flush(); fOut.close(); } }catch(Exception e) { e.printStackTrace(); }
}
publicvoidrotateMyBitmap(Bitmap bmp,String name){ //*****旋转一下 Matrix matrix =newMatrix(); matrix.postRotate(270);
Bitmap bitmap = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), Bitmap.Config.RGB_565);
Bitmap nbmp2 = Bitmap.createBitmap(bmp,0,0, bmp.getWidth(), bmp.getHeight(), matrix,true);
saveMyBitmap(compressImage(nbmp2),"cool");
//*******显示一下 imageView.setImageBitmap(nbmp2);
}; /** * 压缩图片 * *@paramimage *@return */ publicstaticBitmap compressImage(Bitmap image) { ByteArrayOutputStream baos =newByteArrayOutputStream(); // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 image.compress(Bitmap.CompressFormat.JPEG,100, baos); // 把压缩后的数据baos存放到ByteArrayInputStream中 ByteArrayInputStream isBm =newByteArrayInputStream(baos.toByteArray()); // 把ByteArrayInputStream数据生成图片 Bitmap bitmap = BitmapFactory.decodeStream(isBm,null,null); returnbitmap; }
Camera.ShutterCallback shutterCallback =newCamera.ShutterCallback() { @Override publicvoidonShutter() { } };
Camera.PictureCallback rawPictureCallback =newCamera.PictureCallback() { @Override publicvoidonPictureTaken(byte[] arg0, Camera arg1) {
} };
Camera.PictureCallback jpegPictureCallback =newCamera.PictureCallback() { @Override publicvoidonPictureTaken(byte[] arg0, Camera arg1) {
String fileName = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) .toString() + File.separator +"PicTest_"+ System.currentTimeMillis() +".jpg"; File file =newFile(fileName); if(!file.getParentFile().exists()) { file.getParentFile().mkdir(); }
try{ BufferedOutputStream bos =newBufferedOutputStream( newFileOutputStream(file)); bos.write(arg0); bos.flush(); bos.close(); scanFileToPhotoAlbum(file.getAbsolutePath()); Toast.makeText(MainActivity.this,"[Test] Photo take and store in"+ file.toString(),Toast.LENGTH_LONG).show(); }catch(Exception e) { Toast.makeText(MainActivity.this,"Picture Failed"+ e.toString(), Toast.LENGTH_LONG).show(); } }; };
publicvoidsetVolumnSilence(){ manager = (AudioManager)this .getSystemService(Context.AUDIO_SERVICE); manager.setStreamMute(AudioManager.STREAM_SYSTEM,false); volumn = manager.getStreamVolume(AudioManager.STREAM_SYSTEM); if(volumn !=0) { // 如果需要静音并且当前未静音(muteMode的设置可以放在Preference中) manager.setStreamVolume(AudioManager.STREAM_SYSTEM,0, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); } }
publicvoidscanFileToPhotoAlbum(String path) {
MediaScannerConnection.scanFile(MainActivity.this, newString[] { path },null, newMediaScannerConnection.OnScanCompletedListener() {
publicvoidonScanCompleted(String path, Uri uri) { Log.i("TAG","Finished scanning "+ path); } }); }
publicvoidcameraRefresh(String picPath) { Toast.makeText(this,picPath,Toast.LENGTH_SHORT).show(); }
privatefinalclassSurfaceViewCallbackimplementsandroid.view.SurfaceHolder.Callback { publicvoidsurfaceChanged(SurfaceHolder arg0,intarg1,intarg2,intarg3) { if(previewing) { mCamera.stopPreview(); previewing =false; }
try{ mCamera.setPreviewDisplay(arg0); mCamera.startPreview(); previewing =true; setCameraDisplayOrientation(MainActivity.this, mCurrentCamIndex, mCamera); }catch(Exception e) {} } publicvoidsurfaceCreated(SurfaceHolder holder) { // mCamera = Camera.open(); //change to front camera mCamera = openFrontFacingCameraGingerbread(); // get Camera parameters Camera.Parameters params = mCamera.getParameters();
List if(focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { // Autofocus mode is supported } mCamera.setPreviewCallback(newCamera.PreviewCallback() { @Override publicvoidonPreviewFrame(byte[] bytes, Camera camera) { Log.e("stuart","onPreviewFrame "+canTake); if(canTake) { getSurfacePic(bytes, camera,"hahahaah"); canTake=false; } } }); }
publicvoidsurfaceDestroyed(SurfaceHolder holder) { mCamera.stopPreview(); mCamera.release(); mCamera =null; previewing =false; }
}
privateCamera openFrontFacingCameraGingerbread() { intcameraCount =0; Camera cam =null; Camera.CameraInfo cameraInfo =newCamera.CameraInfo(); cameraCount = Camera.getNumberOfCameras();
for(intcamIdx =0; camIdx < cameraCount; camIdx++) { Camera.getCameraInfo(camIdx, cameraInfo); if(cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { try{ cam = Camera.open(camIdx); mCurrentCamIndex = camIdx; }catch(RuntimeException e) { Log.e(TAG,"Camera failed to open: "+ e.getLocalizedMessage()); } } }
returncam; }
privatestaticvoidsetCameraDisplayOrientation(Activity activity,intcameraId, Camera camera) { Camera.CameraInfo info =newCamera.CameraInfo(); Camera.getCameraInfo(cameraId, info); introtation = activity.getWindowManager().getDefaultDisplay().getRotation();
//degrees the angle that the picture will be rotated clockwise. Valid values are 0, 90, 180, and 270. //The starting position is 0 (landscape). intdegrees =0; switch(rotation) { caseSurface.ROTATION_0: degrees =0;break; caseSurface.ROTATION_90: degrees =90;break; caseSurface.ROTATION_180: degrees =180;break; caseSurface.ROTATION_270: degrees =270;break; } intresult; if(info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { result = (info.orientation + degrees) %360; result = (360- result) %360;// compensate the mirror } else { // back-facing result = (info.orientation - degrees +360) %360; } camera.setDisplayOrientation(result); } } |
基本上呢,这一个代码就能实现简单的静默拍照了。
依旧存在的问题:
图片质量实在有点低。
目前来看这也是没有办法的,因为我只能取到surfaceView的帧图像,而显示在preview中的帧图像质量又是非常感人的。所以不得不说这真是没什么办法。