Android实现后台服务拍照功能

一、背景介绍

最近在项目中遇到一个需求,实现一个后台拍照的功能。一开始在网上寻找解决方案,也尝试了很多种实现方式,都没有满意的方案。不过确定了难点:即拍照要先预览,然后再调用拍照方法。问题也随之而来,既然是要实现后台拍照,就希望能在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" />

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android选择图片或拍照图片上传到服务器

    最近要搞一个项目,需要上传相册和拍照的图片,不负所望,终于完成了!  不过需要说明一下,其实网上很多教程拍照的图片,都是缩略图不是很清晰,所以需要在调用照相机的时候,事先生成一个地址,用于标识拍照的图片URI 具体上传代码: 1.选择图片和上传界面,包括上传完成和异常的回调监听 package com.spring.sky.image.upload; import java.util.HashMap; import java.util.Map; import android.app.Activi

  • Android实现后台服务拍照功能

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

  • Android 无预览拍照功能

    最近得到了一个需求,在后台拍照并保存 public void onTakePhotoClicked() { final SurfaceView preview = new SurfaceView(this); SurfaceHolder holder = preview.getHolder(); // deprecated setting, but required on Android versions prior to 3.0 holder.setType(SurfaceHolder.SUR

  • Android判断后台服务是否开启的两种方法实例详解

    Android判断后台服务是否开启的两种方法实例详解 最近项目用到后台上传,就开启了一个服务service. 但是刚开始用这种方法,有些机型不支持:酷派不支持.然后又换了第二种判断方法. // public boolean isServiceWork(Context mContext, String serviceName) { // boolean isWork = false; // ActivityManager myAM = (ActivityManager) mContext // .

  • android studio后台服务使用详解

    Service 是 Android 系统的服务组件,适用于开发没有用户界面且长时间在后台运行的功能.通过本次试验了解后台服务的基本原理,掌握本地服务的使用方法. 1.创建一个Service服务用来完成简单的求和和比较大小的数学运算.2.创建Activity并调用该数学Service activity_main.xml <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.

  • Android App后台服务报告工作状态实例

    本节讲运行在后台服务里的工作请求,如何向发送请求者报告状态.推荐用LocalBroadcastManager发送和接收状态,它限制了只有本app才能接收到广播. 从IntentService汇报状态 从IntentService发送工作请求状态给其他组件,先创建一个包含状态和数据的Intent.也可以添加action和URI到intent里. 下一步,调用 LocalBroadcastManager.sendBroadcast()发送Intent,应用中所有注册了接收该广播的接收器都能收到.Lo

  • Android自定义Camera实现拍照功能

    本文记录了用自定义Camera实现的简单拍照功能. Camera类在5.0以后不推荐使用了,取而代之的是android.hardware.camera2包下的类,本文使用Camera. 我们首先自定义一个View去继承SurfaceView: public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Camera.AutoFocusCallback { private Surface

  • Android仿微信选择图片和拍照功能

    本文实例为大家分享了 Android微信选择图片的具体代码,和微信拍照功能,供大家参考,具体内容如下 1.Android6.0系统,对于权限的使用都是需要申请,选择图片和拍照需要申请Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE这两个权限. if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageM

  • Android实现手机拍照功能

    本文实例为大家讲解如何轻松实现Android手机拍照功能,分享给大家供大家参考.具体如下: 一.布局文件main.xml <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent"

  • Android调用手机拍照功能的方法

    本文实例讲述了Android调用手机拍照功能的方法.分享给大家供大家参考.具体如下: 一.main.xml布局文件: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" andr

  • Android编程实现拍照功能的2种方法分析

    本文实例讲述了Android编程实现拍照功能的2种方法.分享给大家供大家参考,具体如下: Android系统的照相功能,已实现2种方法,可供大家参考: 1. 调用系统摄像头来拍照 首先,找到AndroidManifest.xml文件里加入用户权限 <uses-permission android:name="android.permission.CAMERA"></uses-permission> <uses-feature android:name=&q

随机推荐