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" />


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM