原文:https://blog.csdn.net/wurensen/article/details/47024961

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

说明一下,这只是我在摸索中想到的一种解决方案,能很好的解决业务上的需求。对于像很多手机厂商提供的“找回手机”功能时提供的拍照,我不确定他们的实现方式。如果大家有更好的实现方案,不妨交流一下。

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

二、方案介绍
方案实现步骤大致如下:

1.初始化拍照的预览界面(核心部分);
2.在需要拍照时获取相机Camera,并给Camera设置预览界面;
3.打开预览,完成拍照,释放Camera资源(重要)
4.保存、旋转、上传.......(由业务决定)

先大概介绍下业务需求:从用户登录到注销这段时间内,收到后台拍照的指令后完成拍照、保存、上传。以下会基于这个业务场景来详细介绍各步骤的实现。

1.初始化拍照的预览界面
在测试的过程中发现,拍照的预览界面需要在可显示的情况下生成,才能正常拍照,假如是直接创建SurfaceView实例作为预览界面,然后直接调用拍照时会抛出native层的异常:take_failed。想过看源码寻找问题的原因,发现相机核心的功能代码都在native层上面,所以暂且放下,假定的认为该在拍照时该预览界面一定得在最上面一层显示。
由于应用不管是在前台还是按home回到桌面,都需要满足该条件,那这个预览界面应该是全局的,很容易的联想到使用一个全局窗口来作为预览界面的载体。这个全局窗口要是不可见的,不影响后面的界面正常交互。所以,就想到用全局的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();
}
}
}

代码很简单,主要功能就是显示这个窗口、获取用于预览的SurfaceView以及关闭窗口。
在这个业务中,show方法可以直接在自定义的Application类中调用。这样,在应用启动后,窗口就在了,只有在应用销毁(注意,结束所有Activity不会关闭,因为它初始化在Application中,它的生命周期就为应用级的,除非主动调用dismiss方法主动关闭)。
完成了预览界面的初始化,整个实现其实已经非常简单了。可能许多人遇到的问题就是卡在没有预览界面该如何拍照这里,希望这样一种取巧的方式可以帮助大家在以后的项目中遇到无法直接解决问题时,可以考虑从另外的角度切入去解决问题。
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" />

Android后台服务拍照的更多相关文章

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

    一.背景介绍 近期在项目中遇到一个需求.实现一个后台拍照的功能. 一開始在网上寻找解决方式.也尝试了非常多种实现方式,都没有惬意的方案.只是确定了难点:即拍照要先预览,然后再调用拍照方法.问题也随之而 ...

  2. android 后台服务定时通知

    最近有个项目的要求是在程序退出之后,任然可以每天定时发通知,我们可以想下,其实就是后台开一个服务,然后时间到了就发下通知. 1.首先我们需要用到Service类. 先上代码在慢慢解释 package ...

  3. Android中如何像 360 一样优雅的杀死后台服务而不启动

    Android中,虽然有很多方法(API或者shell命令)杀死后台`service`,但是仍然有很多程序几秒内再次启动,导致无法真正的杀死.这里主要着重介绍如何像 360 一样杀死Android后台 ...

  4. Android 三级联动选择城市+后台服务加载数据库

    技术渣,大家将就着看 首先我们需要一个xml数据保存到数据库,这里我从QQ下面找到一个loclist.xml文件 <CountryRegion Name="中国" Code= ...

  5. Android Services (后台服务)

    一.简介 服务是可以在后台执行长时间运行的应用程序组件,它不提供用户界面. 另一个应用程序组件可以启动一个服务,并且即使用户切换到另一个应用程序,它仍然在后台运行. 另外,组件可以绑定到一个服务来与它 ...

  6. Android : App客户端与后台服务的AIDL通信以及后台服务的JNI接口实现

    一.APP客户端进程与后台服务进程的AIDL通信 AIDL(Android Interface definition language-“接口定义语言”) 是 Android 提供的一种进程间通信 ( ...

  7. Android后台保活实践总结:即时通讯应用无法根治的“顽疾”

    前言 Android进程和Service的保活,是困扰Android开发人员的一大顽疾.因涉及到省电和内存管理策略,各厂商基于自家的理解,在自已ROOM发布于都对标准Android发行版作为或多或少的 ...

  8. Android本地服务

    一.服务生命周期总结 (一).单独开启服务,并没有绑定服务Activity中调用startService(),服务的lifecycle:onCreate()→onStartCommand()→onSt ...

  9. 服务 IntentService 前台服务 定时后台服务

    Activity public class MainActivity extends ListActivity {     private int intentNumber = 0;     @Ove ...

随机推荐

  1. HashMap 源码解析(一)之使用、构造以及计算容量

    目录 简介 集合和映射 HashMap 特点 使用 构造 相关属性 构造方法 tableSizeFor 函数 一般的算法(效率低, 不值得借鉴) tableSizeFor 函数算法 效率比较 tabl ...

  2. Unity协程Coroutine使用总结和一些坑

    原文摘自 Unity协程Coroutine使用总结和一些坑 MonoBehavior关于协程提供了下面几个接口: 可以使用函数或者函数名字符串来启动一个协程,同时可以用函数,函数名字符串,和Corou ...

  3. 关于OBS获取显示器黑屏的解决办法

    近来看到许多人说OBS获取显示器源的时候黑屏,下面介绍下相关处理办法. 第一种,先尝试把OBS程序的兼容性设置成Win 7和管理员身份,具体操作: 设置成这样,如果能够获取到显示器,那么问题解决,否则 ...

  4. MySQL数据库--外键约束及外键使用

    什么是主键.外键关系型数据库中的一条记录中有若干个属性,若其中某一个属性组(注意是组)能唯一标识一条记录,该属性组就可以成为一个主键. 比如: 学生表(学号,姓名,性别,班级) 其中每个学生的学号是唯 ...

  5. kafka学习总结之kafka核心

    1.  Kafka核心组件 (1)replication(副本).partition(分区) 一个topic可以有多个副本,副本的数量决定了有多少个broker存放写入的数据:副本是以partitio ...

  6. Varnish 4.0 实战

    简介 Varnish 是一款高性能且开源的反向代理服务器和 HTTP 加速器,其采用全新的软件体系机构,和现在的硬件体系紧密配合,与传统的 squid 相比,varnish 具有性能更高.速度更快.管 ...

  7. 12th 对礼物挑选小工具的WBS功能分解

    WBS功能分解: 功能 一级子功能 二级子功能 预计用时(分钟) 主页 进入相应页面 1.如果用户已处于登录状态,则返回用户登录时的主页 10 2.如果用户处于未登录状态,则返回用户预览主页. 10 ...

  8. ESXi主机性能问题

    服务器遇到一个问题 百度了下 基本发现是 四路的 windows 服务器的问题. 造成一些 性能降低. 然后查看了下几个虚拟机 的确是设置的4个虚拟插槽 根据百度的结果 要么改配置文件 要么改 这个四 ...

  9. Aqua Data Studio 数据库开发工具

    Aqua Data Studio是一款完整IDE的数据库开发工具,它提供3种主要功能:数据查询与管理工具.比对数据工具与源控制和文件系统的整合工具.帮助你创建,编辑和执行 SQL 的管理工具脚本编写, ...

  10. Dubbo和Spring Cloud微服务架构比较

    Dubbo 出生于阿里系,是阿里巴巴服务化治理的核心框架,并被广泛应用于中国各互联网公司:只需要通过 Spring 配置的方式即可完成服务化,对于应用无入侵,设计的目的还是服务于自身的业务为主. 微服 ...