Android后台服务拍照

原文: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" />

原文地址:https://www.cnblogs.com/hei-hei-hei/p/10616727.html

时间: 2024-10-12 16:01:09

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

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

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

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

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

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中如何像 360 一样优雅的杀死后台服务而不启动

Android中,虽然有很多方法(API或者shell命令)杀死后台`service`,但是仍然有很多程序几秒内再次启动,导致无法真正的杀死.这里主要着重介绍如何像 360 一样杀死Android后台服务,而不会再次启动. 一.已知的 kill 后台应用程序的方法 android.os.Process.killProcess(pid); activityManager.killBackgroundProcesses(pkgName); kill -9 pid 这三种方法都可以“杀死”后台应用程序

android如何做到类似于微信那样后台服务不会被杀死?

问题描述 正在做一款锁屏应用. 做锁屏肯定用到了service,可是我发现每日手动点击自带的内存清理按钮的时候,我的那个service总是会被杀死. 而微信的后台服务却是一直正常的运行,不会被杀掉. 360的话也不会被杀死,但是360会重新启动.而且360的是两个后台服务,我猜有可能会相互作用的,杀死一个的时候另一个接收到广播把其重启. 尝试过用startForeground以及提高service优先级的方式,发现都不行,service都会被杀死. 反编译了一下微信的代码. 请大家帮忙想想办法吧

Android四大组件——Service后台服务、前台服务、IntentService、跨进程服务、无障碍服务、系统服务

Service后台服务.前台服务.IntentService.跨进程服务.无障碍服务.系统服务 本篇文章包括以下内容: 前言 Service的简介 后台服务 不可交互的后台服务 可交互的后台服务 混合性交互的后台服务 前台服务 IntentService AIDL跨进程服务 AccessibilityService无障碍服务 系统服务 部分源码下载 前言 作为四大组件之一的Service类,是面试和笔试的必备关卡,我把我所学到的东西总结了一遍,相信你看了之后你会对Service娓娓道来,在以后遇

Android Services (后台服务)

一.简介 服务是可以在后台执行长时间运行的应用程序组件,它不提供用户界面. 另一个应用程序组件可以启动一个服务,并且即使用户切换到另一个应用程序,它仍然在后台运行. 另外,组件可以绑定到一个服务来与它进行交互,甚至执行进程间通信(IPC). 例如,服务可以从后台处理网络交易,播放音乐,执行文件I / O或与内容提供商交互. 这些是三种不同类型的服务: Scheduled(计划的服务)--- Android 5.0后可用当在Android 5.0(API级别21)中引入的诸如JobSchedule