How to create hidden camera code for Android spy app

android spy cam app

Spy app is a popular topic amongst Android users. Today I’ll show you the code for creating a spy app that will take photos silently, without any preview, every 5 minutes (for example, it’s customizable). We will use android.hardware.Camera class and AlarmManager to set repeated timer.

This is what we will do today:

spy app

Let’s start with project setup. For this spy app we will need only one activity that will display list of photos taken by our spy app, and time the photo was made.¬†Create an empty project and leave it with MainActivity as it set by default. And after a few seconds we have ready-to-use empty project, and let’s fill it with some code!

Layouts

First of all, we’ll create xml layout for our MainActivity with a ListView that will contain list of taken photos. Open activity_main.xml (generated by default) and change RelativeLayout to FrameLayout. It’s lightweight, and we’d better use this one, because we have only view inside and we don’t need all features of RelativeLayout class. After that, delete TextView saying ‘Hello world!’ and add a ListView, so that activity_main.xml looks as follows:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.nerdgrl.spycamera.MainActivity">

    <ListView
        android:id="@+id/list_photos"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

Let’s also create xml layout for list item that will display a single photo and a date. Make a new file in ‘layout’ folder named ‘item_photo.xml’:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:gravity="center_vertical">
    
    <ImageView
        android:id="@+id/photo_image"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginRight="8dp"
        tools:src="@mipmap/ic_launcher"/>
    
    <TextView
        android:id="@+id/photo_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="16sp"
        tools:text="8:00 am\nJune, 25"/>

</LinearLayout>

You can see I’m using ‘tools’ namespace here. It’s a very useful Android editor’s feature that allows you to preview your xml layout with some content without changing actual layout file (that will go into build). If you set tools:text for a TextView, this text will be visible only inside the editor. Your TextView in the app won’t be changed.

Alright, layouts are ready, let’s just set ListView variable in MainActivity class, and we can start working with Camera. Add these line to the onCreate() method:

lvPhotos = (ListView) findViewById(R.id.list_photos);

And define a private variable for the list:

private ListView lvPhotos;

Camera Manager

We’ll start with creating a special class that will keep all logic for the Camera class. Let’s call it CameraManager. And let’s make it a singleton. Here is how we do it:

1. Create a private constructor that takes Context as an argument.
2. Add a private static variable for holding CameraManager instance.
3. Create a getInstance() method that checks if our CameraManager was created or not and returns it.

It should look as follows:

public class CameraManager {

    public static CameraManager getInstance(Context context) {
        if(mManager == null) mManager = new CameraManager(context);
        return mManager;
    }

    private CameraManager(Context context) {
        mContext = context;
    }

    private static CameraManager mManager;

    private Context mContext;

}

This will be the lifecycle of our requests to Camera API:

spy app camera lifecycle

Let’s start from the beginning and create main public method that will be used in our spy app. We call it takePhoto():

public void takePhoto() {
    if(isBackCameraAvailable()) {
        initCamera();
    }
}

As you can see, we check here if current device has back camera or not. We’ll use it for taking high quality photos, because front camera usually offers worst results. Here is the code for isBackCameraAvailable() method:

private boolean isBackCameraAvailable() {
    boolean result = false;
    if(mContext != null && 
            mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
        int numberOfCameras = Camera.getNumberOfCameras();
        for (int i = 0; i < numberOfCameras; i++) {
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(i, info);
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                result = true;
                break;
            }
        }
    }
    return result;
}

In this method we fetch list of all cameras available on the device and check if one of them has id equals to CAMERA_FACING_BACK.

Camera Setup

Now we can start using the camera in our spy app, and let's initialize it first:

private void initCamera() {
    new AsyncTask() {

        @Override
        protected Void doInBackground(Void... voids) {
            try {
                mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
            } catch (RuntimeException e) {
                Log.e(TAG, "Cannot open camera");
                e.printStackTrace();
                isWorking = false;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            try {
                if(mCamera != null) {
                    mSurface = new SurfaceTexture(123);
                    mCamera.setPreviewTexture(mSurface);

                    Camera.Parameters params = mCamera.getParameters();
                    int angle = 270;//getCameraRotationAngle(Camera.CameraInfo.CAMERA_FACING_BACK, mCamera);
                    params.setRotation(angle);

                    if (autoFocusSupported(mCamera)) {
                        params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
                    } else {
                        Log.w(TAG, "Autofocus is not supported");
                    }

                    mCamera.setParameters(params);
                    mCamera.setPreviewCallback(CameraManager.this);
                    mCamera.setErrorCallback(CameraManager.this);
                    mCamera.startPreview();
                    muteSound();
                }
            } catch (IOException e) {
                Log.e(TAG, "Cannot set preview for the front camera");
                e.printStackTrace();
                releaseCamera();
            }
        }

    }.execute();
}

This code is a bit complicated and forces us to add many other methods, so let's analyze it line by line.

First, we open a connection to our back camera:

mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);

If camera is used by another application, this method will throw RuntimeException which we handle in try-catch block. open() method can take long time to execute, so we'll use it in a background thread.

Next, we create a surface for displaying a preview for the camera. But our spy app shouldn't show any preview, so it's just a stub SurfaceTexture object with some random texObject id passed as a parameter:

mSurface = new SurfaceTexture(123);
mCamera.setPreviewTexture(mSurface);

setPreviewTexture() method can throw IOException if the texture unavailable or unsuitable, and thus we need to handle this case in try-catch block, and release camera necessarily (releaseCamera() method will be described later on):

try {...}
catch(IOException e) {
    Log.e(TAG, "Cannot set preview for the front camera");
    e.printStackTrace();
    releaseCamera();
}

After that we configure the camera by setting up all camera parameters:

Camera.Parameters params = mCamera.getParameters();
int angle = getCameraRotationAngle(Camera.CameraInfo.CAMERA_FACING_FRONT, mCamera);
params.setRotation(angle);
if (autoFocusSupported(mCamera)) {
    params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
} else {
    Log.w(TAG, "Autofocus is not supported");
}
mCamera.setParameters(params);

We calculate appropriate rotation angle with getCameraRotationAngle() method. First we get current rotation value of device's display, and rotation angle of the camera. And make a simple calculation to adjust camera:

public int getCameraRotationAngle(int cameraId, android.hardware.Camera camera) {
    int result = 270;
    if(camera != null && mContext != null) {
        Camera.CameraInfo info = new Camera.CameraInfo();
        Camera.getCameraInfo(cameraId, info);
        int rotation = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
        int degrees = getRotationAngle(rotation);

        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360; //compensates mirroring
        }
    }
    return result;
}

private int getRotationAngle(int rotation) {
    int degrees = 0;
    switch (rotation) {
        case Surface.ROTATION_0:
            degrees = 0;
            break;
        case Surface.ROTATION_90:
            degrees = 90;
            break;
        case Surface.ROTATION_180:
            degrees = 180;
            break;
        case Surface.ROTATION_270:
            degrees = 270;
            break;
    }
    return degrees;
}

After that we check if current camera has an autofocus feature by using this method:

private boolean autoFocusSupported(Camera camera) {
    if(camera != null) {
        Camera.Parameters params = camera.getParameters();
        List focusModes = params.getSupportedFocusModes();
        if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
            return true;
        }
    }
    return false;
}

Next, we need to setup preview callback and error callback (it's very useful for checking why camera is not working):

mCamera.setPreviewCallback(this);
mCamera.setErrorCallback(this);

This forces us to let CameraManager implement Camera.ErrorCallback and Camera.PreviewCallback:

public class CameraManager implements Camera.ErrorCallback, Camera.PreviewCallback { ... }

We can handle ErrorCallback this way:

@Override
public void onError(int error, Camera camera) {
    switch (error) {
        case Camera.CAMERA_ERROR_SERVER_DIED:
            Log.e(TAG, "Camera error: Media server died");
            break;
        case Camera.CAMERA_ERROR_UNKNOWN:
            Log.e(TAG, "Camera error: Unknown");
            break;
        case Camera.CAMERA_ERROR_EVICTED:
            Log.e(TAG, "Camera error: Camera was disconnected due to use by higher priority user");
            break;
        default:
            Log.e(TAG, "Camera error: no such error id (" + error + ")");
            break;
    }
}

Let's finish with initializing camera and I'll show code for PreviewCallback. After setting up callback we can finally start camera preview and explicitly mute shutter sound:

mCamera.startPreview();
muteSound();

Here is how the sound can be muted:

private void muteSound() {
    if(mContext != null) {
        AudioManager mgr = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mgr.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_MUTE, 0);
        } else {
            mgr.setStreamMute(AudioManager.STREAM_SYSTEM, true);
        }
    }
}

And finally, this is the releaseCamera() method for our try-catch block (we release both camera and surface objects, and unmute system sounds, because muting was system-wide):

private void releaseCamera() {
    if(mCamera != null) {
        mCamera.release();
        mSurface.release();
        mCamera = null;
        mSurface = null;
    }
    unmuteSound();
}
private void unmuteSound() {
    if(mContext != null) {
        AudioManager mgr = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mgr.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_UNMUTE, 0);
        } else {
            mgr.setStreamMute(AudioManager.STREAM_SYSTEM, false);
        }
    }
}

Taking Photos

This was a long setup preparation, but now we can go to the most interesting part - taking real photos in the spy app! Our CameraManager will take photos when the preview is ready, and it means we need to implement onPreviewFrame() method:

@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
    try {
        if(autoFocusSupported(camera)) {
            mCamera.autoFocus(this);
        } else {
            camera.setPreviewCallback(null);
            camera.takePicture(null, null, this);
        }
    } catch (Exception e) {
        Log.e(TAG, "Camera error while taking picture");
        e.printStackTrace();
        releaseCamera();
    }
}

If autofocus is supported - we will take photo after it'll finish focusing, if not - we will take picture right away. You will need to set CameraManager implements Camera.AutoFocusCallback:

public class CameraManager implements Camera.AutoFocusCallback, [...]

as follows (we omit here 'success' variable that tells us if focusing was finished successfully or not, but you can run autofocus method once again if it returned false):

@Override
public void onAutoFocus(boolean success, Camera camera) {
    if(camera != null) {
        try {
            camera.takePicture(null, null, this);
            mCamera.autoFocus(null);
        } catch (Exception e) {
            e.printStackTrace();
            releaseCamera();
        }
    }
}

CameraManager should be able to react somehow when picture is taken, so it needs to implement Camera.PictureCallback:

public class CameraManager implements Camera.PictureCallback, [...]

this way:

@Override
public void onPictureTaken(byte[] bytes, Camera camera) {
    savePicture(bytes);
    releaseCamera();
}

'bytes' array contains image data that will be saved on SD card.

Saving Photos

We will store our photos under /Pictures/SpyApp directory, and name will contain timestamp, so we could parse it to display the date in the ListView later:

private String savePicture(byte[] bytes) {

    String filepath = null;
    try {
        File pictureFileDir = getDir();
        if (bytes == null) {
            Log.e(TAG, "Can't save image - no data");
            return null;
        }
        if (!pictureFileDir.exists() && !pictureFileDir.mkdirs()) {
            Log.e(TAG, "Can't create directory to save image.");
            return null;
        }

        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddhhmmss");
        String date = dateFormat.format(new Date());
        String photoFile = "spyapp_" + date + ".jpg";

        filepath = pictureFileDir.getPath() + File.separator + photoFile;

        File pictureFile = new File(filepath);
        FileOutputStream fos = new FileOutputStream(pictureFile);
        fos.write(bytes);
        fos.close();
        Log.d(TAG, "New image was saved:" + photoFile);

    } catch (Exception e) {
        e.printStackTrace();
    }

    return filepath;
}
private File getDir() {
    File sdDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
    return new File(sdDir, "SpyApp");
}

Scheduled Service

To launch our CameraManager we will use a Service. It will be scheduled by AlarmManager to run every 5 minutes and will take photo every time.

Here is a very simple Service class named CameraService:

public class CameraService extends Service {

    public CameraService() { }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        CameraManager mgr = CameraManager.getInstance(CameraService.this);
        mgr.takePhoto();
        return START_NOT_STICKY;
    }
}

and in MainActivity's onCreate() method we call the following setupAlarmManager() method:

private void setupAlarmManager() {
    PendingIntent pi = PendingIntent.getService(
            this,
            101,
            new Intent(this, CameraService.class),
            PendingIntent.FLAG_UPDATE_CURRENT
    );
    AlarmManager alarmMgr = (AlarmManager) getSystemService(ALARM_SERVICE);
    alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 5 * 60 * 1000, pi);
}

Now it's time to add needed permissions and service tags into the manifest:

<manifest>
	
    ...

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

    <application>

       ...

       <service android:name=".CameraService" android:exported="false" />

    </application>

</manifest>

Populating ListView

To fill our ListView with data we will simply run a background thread checking /Pictures/SpyApp directory and scanning all photos.

But first we need to complete some preparations. Let's create an Adapter to display properly formatting data in the ListView, here is the code (place it in MainActivity class):

class PhotoAdapter extends BaseAdapter {

    @Override
    public int getCount() {
        return mPhotos != null ? mPhotos.length : 0;
    }

    @Override
    public Object getItem(int i) {
        return mPhotos != null ? mPhotos[i] : mPhotos;
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        View root = mInflater.inflate(R.layout.item_photo, viewGroup, false);
        ImageView photo = (ImageView) root.findViewById(R.id.photo_image);
        TextView text = (TextView) root.findViewById(R.id.photo_text);
        File f = (File) getItem(i);
        if(f != null) {
            photo.setImageURI(Uri.parse("file://" + f.getPath()));
            String filename = f.getName();
            String label = null;
            filename = filename.replace("spyapp_", "");
            filename = filename.replace(".jpg", "");
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddhhmmss");
            try {
                Date photoDate = sdf.parse(filename);
                sdf.applyPattern("hh:mm aa\nMM/dd/yyyy");
                label = sdf.format(photoDate);
            } catch (Exception e) {
                e.printStackTrace();
            }
            text.setText(label != null ? label : filename);
        }
        return root;
    }
}

Then, also inside MainActivity, write a method to updatePhotos from /SpyApp directory (which can be called from onCreate() or by clicking a menu option):

private void updatePhotos() {

    new AsyncTask() {

        private FilenameFilter mFilter = new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                return s.endsWith(".jpg");
            }
        };

        @Override
        protected Void doInBackground(Void... voids) {
            File photosDir = CameraManager.getDir();
            if(photosDir.exists() && photosDir.isDirectory())
                mPhotos = photosDir.listFiles(mFilter);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            PhotoAdapter adapter = new PhotoAdapter();
            lvPhotos.setAdapter(adapter);
        }

    }.execute();

}

Now let's run and test our spy app!

How this code work in real spy app - you can check in my Safe Lock app here: Safe Lock

Full source code can be found on GitHub repo

4 Responses to “How to create hidden camera code for Android spy app”

  1. bindu says:

    please keep the source code for hidden camera detector

  2. Kushal says:

    Thanks For the example…

    i downloadded the source code from git hub and tested but i am getting below error .. android 23 im using ..

    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera E/CameraManager: Cannot open camera
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: java.lang.RuntimeException: Fail to connect to camera service
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at android.hardware.Camera.(Camera.java:495)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at android.hardware.Camera.open(Camera.java:356)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at org.nerdgrl.spycamera.CameraManager$1.doInBackground(CameraManager.java:42)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at org.nerdgrl.spycamera.CameraManager$1.doInBackground(CameraManager.java:36)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at android.os.AsyncTask$2.call(AsyncTask.java:295)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at java.util.concurrent.FutureTask.run(FutureTask.java:237)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:234)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    03-03 09:38:59.443 4110-4148/org.nerdgrl.spycamera W/System.err: at java.lang.Thread.run(Thread.java:818)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/CameraBase: An error occurred while connecting to camera: 0
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera E/CameraManager: Cannot open camera
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: java.lang.RuntimeException: Fail to connect to camera service
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at android.hardware.Camera.(Camera.java:495)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at android.hardware.Camera.open(Camera.java:356)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at org.nerdgrl.spycamera.CameraManager$1.doInBackground(CameraManager.java:42)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at org.nerdgrl.spycamera.CameraManager$1.doInBackground(CameraManager.java:36)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at android.os.AsyncTask$2.call(AsyncTask.java:295)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at java.util.concurrent.FutureTask.run(FutureTask.java:237)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:234)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    03-03 09:39:59.488 4110-4170/org.nerdgrl.spycamera W/System.err: at java.lang.Thread.run(Thread.java:818)
    03-03 09:40:59.585 4110-4148/org.nerdgrl.spycamera W/CameraBase: An error occurred while connecting to camera: 0

  3. Vivian says:

    Hi
    This code make this exception:
    java.lang.RuntimeException: Camera is being used after Camera.release() was called

    Thanks.

  4. Deep says:

    If autofocus will also includes flash….spy app uses flashlight while taking picture

Leave a Reply

Your email address will not be published. Required fields are marked *