Android后台服务拍照的解决方式

一、背景介绍

近期在项目中遇到一个需求。实现一个后台拍照的功能。

一開始在网上寻找解决方式。也尝试了非常多种实现方式,都没有惬意的方案。只是确定了难点:即拍照要先预览,然后再调用拍照方法。问题也随之而来。既然是要实现后台拍照,就希望能在Service中或者是异步的线程中进行,这和预览这个步骤有点相矛盾。

那有什么方式可以既能正常的实现预览、拍照,又不让使用者察觉呢?想必大家也会想到一个取巧的办法:隐藏预览界面。

说明一下,这仅仅是我在摸索中想到的一种解决方式。能非常好的解决业务上的需求。

对于像非常多手机厂商提供的“找回手机”功能时提供的拍照。我不确定他们的实现方式。假设大家有更好的实现方案。最好还是交流一下。

关于这个功能是否侵犯了用户的隐私,影响用户的安全等等问题,不在我们的考虑和讨论范围之内。

二、方案介绍

方案实现步骤大致例如以下:

1.初始化拍照的预览界面(核心部分);

2.在须要拍照时获取相机Camera,并给Camera设置预览界面;

3.打开预览。完毕拍照。释放Camera资源(重要)

4.保存、旋转、上传.......(由业务决定)

先大概介绍下业务需求:从用户登录到注销这段时间内,收到后台拍照的指令后完毕拍照、保存、上传。

下面会基于这个业务场景来具体介绍各步骤的实现。

1.初始化拍照的预览界面

在測试的过程中发现,拍照的预览界面须要在可显示的情况下生成,才干正常拍照,假如是直接创建SurfaceView实例作为预览界面。然后直接调用拍照时会抛出native层的异常:take_failed。想过看源代码寻找问题的解决办法。发现相机核心的功能代码都在native层上面,所以暂且放下。假定的觉得该在拍照时该预览界面一定得在最上面一层显示。

因为应用无论是在前台还是按home回到桌面,都须要满足该条件,那这个预览界面应该是全局的,非常easy的联想到使用一个全局窗体来作为预览界面的载体。

这个全局窗体要是不可见的。不影响后面的界面正常交互。所以。就想到用全局的context来获取WindowManager对象管理这个全局窗体。

接下来直接看代码:

package com.yuexunit.zjjk.service;

import com.yuexunit.zjjk.util.Logger;

import android.content.Context;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;

/**
 * 隐藏的全局窗体。用于后台拍照
 *
 * @author WuRS
 */
public class CameraWindow {

	private static final String TAG = CameraWindow.class.getSimpleName();

	private static WindowManager windowManager;

	private static Context applicationContext;

	private static SurfaceView dummyCameraView;

	/**
	 * 显示全局窗体
	 *
	 * @param context
	 */
	public static void show(Context context) {
		if (applicationContext == null) {
			applicationContext = context.getApplicationContext();
			windowManager = (WindowManager) applicationContext
					.getSystemService(Context.WINDOW_SERVICE);
			dummyCameraView = new SurfaceView(applicationContext);
			LayoutParams params = new LayoutParams();
			params.width = 1;
			params.height = 1;
			params.alpha = 0;
			params.type = LayoutParams.TYPE_SYSTEM_ALERT;
			// 屏蔽点击事件
			params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
					| LayoutParams.FLAG_NOT_FOCUSABLE
					| LayoutParams.FLAG_NOT_TOUCHABLE;
			windowManager.addView(dummyCameraView, params);
			Logger.d(TAG, TAG + " showing");
		}
	}

	/**
	 * @return 获取窗体视图
	 */
	public static SurfaceView getDummyCameraView() {
		return dummyCameraView;
	}

	/**
	 * 隐藏窗体
	 */
	public static void dismiss() {
		try {
			if (windowManager != null && dummyCameraView != null) {
				windowManager.removeView(dummyCameraView);
				Logger.d(TAG, TAG + " dismissed");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

代码非常easy。主要功能就是显示这个窗体、获取用于预览的SurfaceView以及关闭窗体。

在这个业务中,show方法能够直接在自己定义的Application类中调用。这样。在应用启动后,窗体就在了,仅仅有在应用销毁(注意,结束全部Activity不会关闭,由于它初始化在Application中,它的生命周期就为应用级的,除非主动调用dismiss方法主动关闭)。

完毕了预览界面的初始化。整个实现事实上已经很easy了。

可能很多人遇到的问题就是卡在没有预览界面该怎样拍照这里,希望这样一种取巧的方式能够帮助大家在以后的项目中遇到无法直接解决这个问题时。能够考虑从另外的角度切入去解决这个问题。

2.完毕Service拍照功能

这里将对上面的兴许步骤进行合并。

先上代码:

package com.yuexunit.zjjk.service;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PictureCallback;
import android.os.IBinder;
import android.os.Message;
import android.text.TextUtils;
import android.view.SurfaceView;

import com.yuexunit.sortnetwork.android4task.UiHandler;
import com.yuexunit.sortnetwork.task.TaskStatus;
import com.yuexunit.zjjk.network.RequestHttp;
import com.yuexunit.zjjk.util.FilePathUtil;
import com.yuexunit.zjjk.util.ImageCompressUtil;
import com.yuexunit.zjjk.util.Logger;
import com.yuexunit.zjjk.util.WakeLockManager;

/**
 * 后台拍照服务。配合全局窗体使用
 *
 * @author WuRS
 */
public class CameraService extends Service implements PictureCallback {

	private static final String TAG = CameraService.class.getSimpleName();

	private Camera mCamera;

	private boolean isRunning; // 是否已在监控拍照

	private String commandId; // 指令ID

	@Override
	public void onCreate() {
		Logger.d(TAG, "onCreate...");
		super.onCreate();
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		WakeLockManager.acquire(this);
		Logger.d(TAG, "onStartCommand...");
		startTakePic(intent);
		return START_NOT_STICKY;
	}

	private void startTakePic(Intent intent) {
		if (!isRunning) {
			commandId = intent.getStringExtra("commandId");
			SurfaceView preview = CameraWindow.getDummyCameraView();
			if (!TextUtils.isEmpty(commandId) && preview != null) {
				autoTakePic(preview);
			} else {
				stopSelf();
			}
		}
	}

	private void autoTakePic(SurfaceView preview) {
		Logger.d(TAG, "autoTakePic...");
		isRunning = true;
		mCamera = getFacingFrontCamera();
		if (mCamera == null) {
			Logger.w(TAG, "getFacingFrontCamera return null");
			stopSelf();
			return;
		}
		try {
			mCamera.setPreviewDisplay(preview.getHolder());
			mCamera.startPreview();// 開始预览
			// 防止某些手机拍摄的照片亮度不够
			Thread.sleep(200);
			takePicture();
		} catch (Exception e) {
			e.printStackTrace();
			releaseCamera();
			stopSelf();
		}
	}

	private void takePicture() throws Exception {
		Logger.d(TAG, "takePicture...");
		try {
			mCamera.takePicture(null, null, this);
		} catch (Exception e) {
			Logger.d(TAG, "takePicture failed!");
			e.printStackTrace();
			throw e;
		}
	}

	private Camera getFacingFrontCamera() {
		CameraInfo cameraInfo = new CameraInfo();
		int numberOfCameras = Camera.getNumberOfCameras();
		for (int i = 0; i < numberOfCameras; i++) {
			Camera.getCameraInfo(i, cameraInfo);
			if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
				try {
					return Camera.open(i);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}

	@Override
	public void onPictureTaken(byte[] data, Camera camera) {
		Logger.d(TAG, "onPictureTaken...");
		releaseCamera();
		try {
			// 大于500K,压缩预防内存溢出
			Options opts = null;
			if (data.length > 500 * 1024) {
				opts = new Options();
				opts.inSampleSize = 2;
			}
			Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length,
					opts);
			// 旋转270度
			Bitmap newBitmap = ImageCompressUtil.rotateBitmap(bitmap, 270);
			// 保存
			String fullFileName = FilePathUtil.getMonitorPicPath()
					+ System.currentTimeMillis() + ".jpeg";
			File saveFile = ImageCompressUtil.convertBmpToFile(newBitmap,
					fullFileName);
			ImageCompressUtil.recyleBitmap(newBitmap);
			if (saveFile != null) {
				// 上传
				RequestHttp.uploadMonitorPic(callbackHandler, commandId,
						saveFile);
			} else {
				// 保存失败。关闭
				stopSelf();
			}
		} catch (Exception e) {
			e.printStackTrace();
			stopSelf();
		}
	}

	private UiHandler callbackHandler = new UiHandler() {

		@Override
		public void receiverMessage(Message msg) {
			switch (msg.arg1) {
			case TaskStatus.LISTENNERTIMEOUT:
			case TaskStatus.ERROR:
			case TaskStatus.FINISHED:
				// 请求结束,关闭服务
				stopSelf();
				break;
			}
		}
	};

	// 保存照片
	private boolean savePic(byte[] data, File savefile) {
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(savefile);
			fos.write(data);
			fos.flush();
			fos.close();
			return true;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return false;
	}

	private void releaseCamera() {
		if (mCamera != null) {
			Logger.d(TAG, "releaseCamera...");
			mCamera.stopPreview();
			mCamera.release();
			mCamera = null;
		}
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		Logger.d(TAG, "onDestroy...");
		commandId = null;
		isRunning = false;
		FilePathUtil.deleteMonitorUploadFiles();
		releaseCamera();
		WakeLockManager.release();
	}

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}
}

代码也不多。只是有几个点须要特别注意下,

1.相机在通话时是用不了的。或者别的应用持有该相机时也是获取不到相机的,所以须要捕获camera.Open()的异常。防止获取不到相机时应用出错。

2.在用华为相机測试时。開始预览立刻拍照,发现获取的照片亮度非常低,原因仅仅是推測,详细须要去查资料。所以暂且的解决方式是让线程休眠200ms,然后再调用拍照。

3.在不使用Camera资源或者发生不论什么异常时,请记得释放Camera资源,否则为导致相机被一直持有,别的应用包含系统的相机也用不了,仅仅能重新启动手机解决。

代码大家能够优化下。 把非正常业务逻辑统一处理掉。或者是。使用自己定义的UncaughtExceptionHandler去处理未捕获的异常。

4.关于代码中WakeLocaManager类,是我自己封装的唤醒锁管理类,这也是大家在处理后台关键业务时须要特别关注的一点,保证业务逻辑在处理时。系统不会进入休眠。等业务逻辑处理完。释放唤醒锁,让系统进入休眠。

三、总结

该方案问题也比較多,仅仅是提供一种思路。全局窗体才是这个方法的核心。相机的操作须要慎重,获取的时候须要捕获异常(native异常,连接相机错误。相信大家也遇到过),不使用或异常时及时释放(能够把相机对象写成static,然后在全局的异常捕获中对相机做释放,防止在持有相机这段时间内应用异常时导致相机被异常持有)。不然别的相机应用使用不了。

代码大家稍作改动就能够使用,记得加入相关的权限。下面是系统窗体、唤醒锁、相机的权限。假设用到自己主动对焦再拍照,记得声明下面uses-feature标签。其他经常使用权限这里就不赘述。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

<uses-permission android:name="android.permission.WAKE_LOCK" />

<uses-permission android:name="android.permission.CAMERA" />

时间: 2024-12-10 22:54:36

Android后台服务拍照的解决方式的相关文章

Android后台服务拍照的解决方案

一.背景介绍 最近在项目中遇到一个需求,实现一个后台拍照的功能.一开始在网上寻找解决方案,也尝试了很多种实现方式,都没有满意的方案.不过确定了难点:即拍照要先预览,然后再调用拍照方法.问题也随之而来,既然是要实现后台拍照,就希望能在Service中或者是异步的线程中进行,这和预览这个步骤有点相矛盾.那有什么方式能够既能正常的实现预览.拍照,又不让使用者察觉呢?想必大家也会想到一个取巧的办法:隐藏预览界面. 说明一下,这只是我在摸索中想到的一种解决方案,能很好的解决业务上的需求.对于像很多手机厂商

Android后台服务拍照

原文:https://blog.csdn.net/wurensen/article/details/47024961 一.背景介绍最近在项目中遇到一个需求,实现一个后台拍照的功能.一开始在网上寻找解决方案,也尝试了很多种实现方式,都没有满意的方案.不过确定了难点:即拍照要先预览,然后再调用拍照方法.问题也随之而来,既然是要实现后台拍照,就希望能在Service中或者是异步的线程中进行,这和预览这个步骤有点相矛盾.那有什么方式能够既能正常的实现预览.拍照,又不让使用者察觉呢?想必大家也会想到一个取

BEGINNING SHAREPOINT&amp;#174; 2013 DEVELOPMENT 第13章节--使用业务连接服务创建业务线解决方式 创建启用BCS的业务解决方式

BEGINNING SHAREPOINT? 2013 DEVELOPMENT 第13章节--使用业务连接服务创建业务线解决方式  创建启用BCS的业务解决方式 SP中一个经常使用实践是使用文档库预创建Office文档模板作为内容类型. BEGINNING SHAREPOINT® 2013 DEVELOPMENT 第13章节--使用业务连接服务创建业务线解决方式 创建启用BCS的业务解决方式

mac 系统开发android,真机调试解决方式(无数的坑之后吐血总结)

近期学习android开发,安装了ADT开发环境之后,启动模拟器,慢的要死啊,全然不如苹果的好用,没法,自己买个android手机,准备联机调试程序.没想到在这个过程中,遇到了好多的坑,作为一个新人,每一步都过不去,花了将近2个小时,在万能的百度里查了n多次,最终攻克了.在此,做个记录,希望大家不要走我走过的弯路. 1.确保你的android设备真正链接到电脑上了,我在这里遇到过坑,弄了好久,才发现能充电的线,确无法传递数据过去.所以不要以为随便拿一根线,能充电,就能够传递数据了,我就是这么傻傻

android 后台服务定时通知

最近有个项目的要求是在程序退出之后,任然可以每天定时发通知,我们可以想下,其实就是后台开一个服务,然后时间到了就发下通知. 1.首先我们需要用到Service类. 先上代码在慢慢解释 1 package com.example.androidnotification; 2 3 import java.util.Timer; 4 import java.util.TimerTask; 5 import android.app.Notification; 6 import android.app.N

Android 后台服务简要概述

本篇文章主要讲述android servivce相关知识,其中会穿插一些其他的知识点,作为初学者的教程.老鸟绕路 本文会讲述如下内容: - 为什么要用Service - Service及其继承者IntentService - 一个后台计数器的例子来讲述Service - Service如何与UI组件通信 为什么要用Service 我们接触android的时候,大部分时候是在和activity打交道,但是有些比如网络下载.大文件读取.解析等耗时却又不需要界面对象的操作.一旦退出界面,那么可能就会变

android后台服务的基本用法

了解了安卓多线程编程的技术之后,作为安卓的四大组件之一,是十分重要的. 定义一个服务 首先看一下如何在项目中定义一个服务, public class MyService extends Service { @Override public void onCreate() { super.onCreate(); Log.d("myservice","oncreate"); } @Override public int onStartCommand(Intent int

Android 常见内存泄漏的解决方式

在Android程序开发中.当一个对象已经不须要再使用了,本该被回收时.而另外一个正在使用的对象持有它的引用从而导致它不能被回收.这就导致本该被回收的对象不能被回收而停留在堆内存中,内存泄漏就产生了. 内存泄漏有什么影响呢? 它是造成应用程序OOM的主要原因之中的一个.由于Android系统为每一个应用程序分配的内存有限.当一个应用中产生的内存泄漏比較多时.就难免会导致应用所须要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash. 一.单例造成的内存泄漏 Android的单

Android Studio安装更新终极解决方式

之前写过一篇Android SDK无法更新的博文,其实该方式对Android Studio同样有效,大伙可以下载网盘中分享的小软件,若搜索到通道后提示需要更细,也可以选择更新.参考:http://blog.csdn.net/gao_chun/article/details/37971461 先提一些在安装过程中遇到的问题装完后启动时会显示 Fetching Android SDK component information,这个是在获取SDK组件信息,片刻后在 Setup Wizard - Do