Android後臺服務拍照

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

1、背景介紹
最近在項目中遇到一個需求,實現一個後臺拍照的功能。一開始在網上尋找解決方案,也嘗試了不少種實現方式,都沒有滿意的方案。不過肯定了難點:即拍照要先預覽,而後再調用拍照方法。問題也隨之而來,既然是要實現後臺拍照,就但願能在Service中或者是異步的線程中進行,這和預覽這個步驟有點相矛盾。那有什麼方式可以既能正常的實現預覽、拍照,又不讓使用者察覺呢?想必你們也會想到一個取巧的辦法:隱藏預覽界面。android

說明一下,這只是我在摸索中想到的一種解決方案,能很好的解決業務上的需求。對於像不少手機廠商提供的「找回手機」功能時提供的拍照,我不肯定他們的實現方式。若是你們有更好的實現方案,不妨交流一下。安全

關於這個功能是否侵犯了用戶的隱私,影響用戶的安全等等問題,不在咱們的考慮和討論範圍以內。app

2、方案介紹
方案實現步驟大體以下:異步

1.初始化拍照的預覽界面(核心部分);
2.在須要拍照時獲取相機Camera,並給Camera設置預覽界面;
3.打開預覽,完成拍照,釋放Camera資源(重要)
4.保存、旋轉、上傳.......(由業務決定)ide

先大概介紹下業務需求:從用戶登陸到註銷這段時間內,收到後臺拍照的指令後完成拍照、保存、上傳。如下會基於這個業務場景來詳細介紹各步驟的實現。測試

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拍照功能
這裏將對上面的後續步驟進行合併。先上代碼:ui

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類,是我本身封裝的喚醒鎖管理類,這也是你們在處理後臺關鍵業務時須要特別關注的一點,保證業務邏輯在處理時,系統不會進入休眠。等業務邏輯處理完,釋放喚醒鎖,讓系統進入休眠。
3、總結
該方案問題也比較多,只是提供一種思路。全局窗口才是這個方案的核心。相機的操做須要謹慎,獲取的時候須要捕獲異常(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" />

this

相關文章
相關標籤/搜索