原文: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对象管理这个全局窗口。接下来直接看代码:

  1. package com.yuexunit.zjjk.service;
  2.  
  3. import com.yuexunit.zjjk.util.Logger;
  4.  
  5. import android.content.Context;
  6. import android.view.SurfaceView;
  7. import android.view.WindowManager;
  8. import android.view.WindowManager.LayoutParams;
  9.  
  10. /**
  11. * 隐藏的全局窗口,用于后台拍照
  12. *
  13. * @author WuRS
  14. */
  15. public class CameraWindow {
  16.  
  17. private static final String TAG = CameraWindow.class.getSimpleName();
  18.  
  19. private static WindowManager windowManager;
  20.  
  21. private static Context applicationContext;
  22.  
  23. private static SurfaceView dummyCameraView;
  24.  
  25. /**
  26. * 显示全局窗口
  27. *
  28. * @param context
  29. */
  30. public static void show(Context context) {
  31. if (applicationContext == null) {
  32. applicationContext = context.getApplicationContext();
  33. windowManager = (WindowManager) applicationContext
  34. .getSystemService(Context.WINDOW_SERVICE);
  35. dummyCameraView = new SurfaceView(applicationContext);
  36. LayoutParams params = new LayoutParams();
  37. params.width = 1;
  38. params.height = 1;
  39. params.alpha = 0;
  40. params.type = LayoutParams.TYPE_SYSTEM_ALERT;
  41. // 屏蔽点击事件
  42. params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
  43. | LayoutParams.FLAG_NOT_FOCUSABLE
  44. | LayoutParams.FLAG_NOT_TOUCHABLE;
  45. windowManager.addView(dummyCameraView, params);
  46. Logger.d(TAG, TAG + " showing");
  47. }
  48. }
  49.  
  50. /**
  51. * @return 获取窗口视图
  52. */
  53. public static SurfaceView getDummyCameraView() {
  54. return dummyCameraView;
  55. }
  56.  
  57. /**
  58. * 隐藏窗口
  59. */
  60. public static void dismiss() {
  61. try {
  62. if (windowManager != null && dummyCameraView != null) {
  63. windowManager.removeView(dummyCameraView);
  64. Logger.d(TAG, TAG + " dismissed");
  65. }
  66. } catch (Exception e) {
  67. e.printStackTrace();
  68. }
  69. }
  70. }

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

  1. package com.yuexunit.zjjk.service;
  2.  
  3. import java.io.File;
  4. import java.io.FileNotFoundException;
  5. import java.io.FileOutputStream;
  6. import java.io.IOException;
  7.  
  8. import android.app.Service;
  9. import android.content.Intent;
  10. import android.graphics.Bitmap;
  11. import android.graphics.BitmapFactory;
  12. import android.graphics.BitmapFactory.Options;
  13. import android.hardware.Camera;
  14. import android.hardware.Camera.CameraInfo;
  15. import android.hardware.Camera.PictureCallback;
  16. import android.os.IBinder;
  17. import android.os.Message;
  18. import android.text.TextUtils;
  19. import android.view.SurfaceView;
  20.  
  21. import com.yuexunit.sortnetwork.android4task.UiHandler;
  22. import com.yuexunit.sortnetwork.task.TaskStatus;
  23. import com.yuexunit.zjjk.network.RequestHttp;
  24. import com.yuexunit.zjjk.util.FilePathUtil;
  25. import com.yuexunit.zjjk.util.ImageCompressUtil;
  26. import com.yuexunit.zjjk.util.Logger;
  27. import com.yuexunit.zjjk.util.WakeLockManager;
  28.  
  29. /**
  30. * 后台拍照服务,配合全局窗口使用
  31. *
  32. * @author WuRS
  33. */
  34. public class CameraService extends Service implements PictureCallback {
  35.  
  36. private static final String TAG = CameraService.class.getSimpleName();
  37.  
  38. private Camera mCamera;
  39.  
  40. private boolean isRunning; // 是否已在监控拍照
  41.  
  42. private String commandId; // 指令ID
  43.  
  44. @Override
  45. public void onCreate() {
  46. Logger.d(TAG, "onCreate...");
  47. super.onCreate();
  48. }
  49.  
  50. @Override
  51. public int onStartCommand(Intent intent, int flags, int startId) {
  52. WakeLockManager.acquire(this);
  53. Logger.d(TAG, "onStartCommand...");
  54. startTakePic(intent);
  55. return START_NOT_STICKY;
  56. }
  57.  
  58. private void startTakePic(Intent intent) {
  59. if (!isRunning) {
  60. commandId = intent.getStringExtra("commandId");
  61. SurfaceView preview = CameraWindow.getDummyCameraView();
  62. if (!TextUtils.isEmpty(commandId) && preview != null) {
  63. autoTakePic(preview);
  64. } else {
  65. stopSelf();
  66. }
  67. }
  68. }
  69.  
  70. private void autoTakePic(SurfaceView preview) {
  71. Logger.d(TAG, "autoTakePic...");
  72. isRunning = true;
  73. mCamera = getFacingFrontCamera();
  74. if (mCamera == null) {
  75. Logger.w(TAG, "getFacingFrontCamera return null");
  76. stopSelf();
  77. return;
  78. }
  79. try {
  80. mCamera.setPreviewDisplay(preview.getHolder());
  81. mCamera.startPreview();// 开始预览
  82. // 防止某些手机拍摄的照片亮度不够
  83. Thread.sleep(200);
  84. takePicture();
  85. } catch (Exception e) {
  86. e.printStackTrace();
  87. releaseCamera();
  88. stopSelf();
  89. }
  90. }
  91.  
  92. private void takePicture() throws Exception {
  93. Logger.d(TAG, "takePicture...");
  94. try {
  95. mCamera.takePicture(null, null, this);
  96. } catch (Exception e) {
  97. Logger.d(TAG, "takePicture failed!");
  98. e.printStackTrace();
  99. throw e;
  100. }
  101. }
  102.  
  103. private Camera getFacingFrontCamera() {
  104. CameraInfo cameraInfo = new CameraInfo();
  105. int numberOfCameras = Camera.getNumberOfCameras();
  106. for (int i = 0; i < numberOfCameras; i++) {
  107. Camera.getCameraInfo(i, cameraInfo);
  108. if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
  109. try {
  110. return Camera.open(i);
  111. } catch (Exception e) {
  112. e.printStackTrace();
  113. }
  114. }
  115. }
  116. return null;
  117. }
  118.  
  119. @Override
  120. public void onPictureTaken(byte[] data, Camera camera) {
  121. Logger.d(TAG, "onPictureTaken...");
  122. releaseCamera();
  123. try {
  124. // 大于500K,压缩预防内存溢出
  125. Options opts = null;
  126. if (data.length > 500 * 1024) {
  127. opts = new Options();
  128. opts.inSampleSize = 2;
  129. }
  130. Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length,
  131. opts);
  132. // 旋转270度
  133. Bitmap newBitmap = ImageCompressUtil.rotateBitmap(bitmap, 270);
  134. // 保存
  135. String fullFileName = FilePathUtil.getMonitorPicPath()
  136. + System.currentTimeMillis() + ".jpeg";
  137. File saveFile = ImageCompressUtil.convertBmpToFile(newBitmap,
  138. fullFileName);
  139. ImageCompressUtil.recyleBitmap(newBitmap);
  140. if (saveFile != null) {
  141. // 上传
  142. RequestHttp.uploadMonitorPic(callbackHandler, commandId,
  143. saveFile);
  144. } else {
  145. // 保存失败,关闭
  146. stopSelf();
  147. }
  148. } catch (Exception e) {
  149. e.printStackTrace();
  150. stopSelf();
  151. }
  152. }
  153.  
  154. private UiHandler callbackHandler = new UiHandler() {
  155.  
  156. @Override
  157. public void receiverMessage(Message msg) {
  158. switch (msg.arg1) {
  159. case TaskStatus.LISTENNERTIMEOUT:
  160. case TaskStatus.ERROR:
  161. case TaskStatus.FINISHED:
  162. // 请求结束,关闭服务
  163. stopSelf();
  164. break;
  165. }
  166. }
  167. };
  168.  
  169. // 保存照片
  170. private boolean savePic(byte[] data, File savefile) {
  171. FileOutputStream fos = null;
  172. try {
  173. fos = new FileOutputStream(savefile);
  174. fos.write(data);
  175. fos.flush();
  176. fos.close();
  177. return true;
  178. } catch (FileNotFoundException e) {
  179. e.printStackTrace();
  180. } catch (IOException e) {
  181. e.printStackTrace();
  182. } finally {
  183. if (fos != null) {
  184. try {
  185. fos.close();
  186. } catch (IOException e) {
  187. e.printStackTrace();
  188. }
  189. }
  190. }
  191. return false;
  192. }
  193.  
  194. private void releaseCamera() {
  195. if (mCamera != null) {
  196. Logger.d(TAG, "releaseCamera...");
  197. mCamera.stopPreview();
  198. mCamera.release();
  199. mCamera = null;
  200. }
  201. }
  202.  
  203. @Override
  204. public void onDestroy() {
  205. super.onDestroy();
  206. Logger.d(TAG, "onDestroy...");
  207. commandId = null;
  208. isRunning = false;
  209. FilePathUtil.deleteMonitorUploadFiles();
  210. releaseCamera();
  211. WakeLockManager.release();
  212. }
  213.  
  214. @Override
  215. public IBinder onBind(Intent intent) {
  216. return null;
  217. }
  218. }

代码也不多,不过有几个点需要特别注意下,
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. LoRa---她的简介和她的专业术语

    LoRa是LPWAN(低功耗广域网)通信技术的一种,其作用距离超过 15 公里,连接节点可达 100 万个.低功耗与长距离极限的组合可将最大数据速率提升至每秒 50千比特(Kbps). LoRa 是  ...

  2. Linux 下的编译安装说明

    https://www.linuxidc.com/Linux/2017-02/140309.htm

  3. python 网络爬虫介绍

    一.网络爬虫相关概念 网络爬虫介绍 我们都知道,当前我们所处的时代是大数据的时代,在大数据时代,要进行数据分析,首先要有数据源,而学习爬虫,可以让我们获取更多的数据源,并且这些数据源可以按我们的目的进 ...

  4. HTML快速入门(一)

    一.HTML 是什么? HTML 指的是超文本标记语言 (Hyper Text Markup Language) HTML 不是一种编程语言,而是一种标记语言 (markup language) 标记 ...

  5. 软件测试_测试工具_Loadrunner_IP欺骗

    一.设置IP欺骗的原因: 1.当某个IP的访问过于频繁或者访问量过大时,服务器会拒绝访问请求: 2.某些服务器配置了负载均衡,使用同一个IP不能测出系统的实际性能.Loadrunner中的IP欺骗通过 ...

  6. 论文阅读 | Clustrophile 2: Guided Visual Clustering Analysis

    论文地址 论文视频 左侧边栏可以导入数据,或者打开以及前保存的结果.右侧显示了所有的日志,可以轻松回到之前的状态,视图的主区域上半部分是数据,下半部分是聚类视图. INTRODUCTION 数据聚类对 ...

  7. Ubuntu16.4下QT配置opencv3.1+FFmpeg

    安装依赖环境 sudo apt-get install build-essential sudo apt-get install cmake git libgtk2.0-dev pkg-config ...

  8. 《杜增强讲Unity之Tanks坦克大战》11-游戏流程控制

    11 游戏流程控制 使用协程来控制游戏流程 11.1 添加MessageText 首先添加一个Text来显示文字   image 设置GameMgr   image 11.2 游戏整体流程 下面Gam ...

  9. 【MOOC EXP】Linux内核分析实验一报告

    程涵  原创博客 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000  [反汇编一个简单的C程序]   实验 ...

  10. Linux内核分析(第六周)

    进程的控制与创建 一.进程的描述 1.操作系统内核的三大功能:进程管理(核心),内存管理,文件系统: 2.状态: fork() task_zombit(终止) task_running(就绪:但是没有 ...