How Surfaceholder Callbacks Are Related to Activity Lifecycle

How SurfaceHolder callbacks are related to Activity lifecycle?

Edit: if the targetSDK is greater than 10, putting the app to sleep calls onPause and onStop. Source

I looked at the lifecycle of both the Activity and the SurfaceView in a tiny camera app on my gingerbread phone. You are entirely correct; the surface is not destroyed when the power button is pressed to put the phone to sleep. When the phone goes to sleep, the Activity does onPause. (And does not do onStop.) It does onResume when the phone wakes up, and, as you point out, it does this while the lock screen is still visible and accepting input, which is a bit odd. When I make the Activity invisible by pressing the Home button, the Activity does both onPause and onStop. Something causes a callback to surfaceDestroyed in this case between the end of onPause and the start of onStop. It's not very obvious, but it does seem very consistent.

When the power button is pressed to sleep the phone, unless something is explicitly done to stop it, the camera keeps running! If I have the camera do a per-image callback for each preview frame, with a Log.d() in there, the log statements keep coming while the phone is pretending to sleep. I think that is Very Sneaky.

As another confusion, the callbacks to surfaceCreated and surfaceChanged happen after onResume in the activity, if the surface is being created.

As a rule, I manage the camera in the class that implements the SurfaceHolder callbacks.

class Preview extends SurfaceView implements SurfaceHolder.Callback {
private boolean previewIsRunning;
private Camera camera;

public void surfaceCreated(SurfaceHolder holder) {
camera = Camera.open();
// ...
// but do not start the preview here!
}

public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// set preview size etc here ... then
myStartPreview();
}

public void surfaceDestroyed(SurfaceHolder holder) {
myStopPreview();
camera.release();
camera = null;
}

// safe call to start the preview
// if this is called in onResume, the surface might not have been created yet
// so check that the camera has been set up too.
public void myStartPreview() {
if (!previewIsRunning && (camera != null)) {
camera.startPreview();
previewIsRunning = true;
}
}

// same for stopping the preview
public void myStopPreview() {
if (previewIsRunning && (camera != null)) {
camera.stopPreview();
previewIsRunning = false;
}
}
}

and then in the Activity:

@Override public void onResume() {
preview.myStartPreview(); // restart preview after awake from phone sleeping
super.onResume();
}
@Override public void onPause() {
preview.myStopPreview(); // stop preview in case phone is going to sleep
super.onPause();
}

and that seems to work OK for me. Rotation events cause the Activity to be destroyed and recreated, which causes the SurfaceView to be destroyed and recreated too.

Different call sequence of the Activity lifecycle callbacks while opening app from the push

There are many related questions on Stackoverflow about onResume() and onPause() being called multiple times in combination with use of the lock screen. This seems to be a common problem. In general, onPause() and onResume() can be called many times, also in quick succession and your app needs to be robust enough to handle that.

See:

  • Android Activity lifecycle and locking/unlocking device
  • onResume being called over and over while phone screen is locked
  • How to prevent or handle onResume being called when phone screen is locked?
  • Locking screen of Android phone results in several subsequent onPause/onResume events
  • Lock screen triggers application lifecycle events of different activity

Should the renderingThread of a SurfaceView have the same life-cycle as the view or the activity?

The Activity and the View are created at essentially the same time. The Surface is created later, and that's what the SufaceHolder callbacks are for.

You can't render on the Surface before it exists or after it's destroyed, so there's no point in starting your rendering thread before then or leaving it running after. The tricky part is that the callbacks happen on the main UI thread (since that's where you set it up), so the surfaceDestroyed() callback could be called while your render thread is doing work.

EDIT:

Some notes about the SurfaceView / Activity lifecycle are included below. These are now part of the official Android documentation; see Appendix B in the System-Level Graphics doc. The original post is available below for historical purposes.

You can see examples of both approaches in Grafika. Approach #1 (create/destroy thread in onResume/onPause) can be seen in TextureFromCameraActivity, approach #2 (create/destroy thread in surfaceCreated/surfaceDestroyed) can be seen in HardwareScalerActivity and RecordFBOActivity.


A few thoughts about app life cycle and SurfaceView.

There are two somewhat independent things going on:

  1. Application onCreate / onResume / onPause
  2. Surface created / changed / destroyed

When the Activity starts, you get callbacks in this order:

  • onCreate
  • onResume
  • surfaceCreated
  • surfaceChanged

If you hit "back", you get:

  • onPause
  • surfaceDestroyed (called just before the Surface goes away)

If you rotate the screen, the Activity is torn down and recreated, so you get
the full cycle. (You can tell it's a "quick" restart by checking isFinishing().) It might be possible to start / stop an activity so quickly that surfaceCreated() might happen after onPause(), but I'm not sure about that.

If you tap the power button to blank the screen, however, you only get onPause() --
no surfaceDestroyed(). The Surface remains alive, and rendering can continue (you
even keep getting Choreographer events if you continue to request them). If you have
a lock screen that forces a specific orientation your Activity can get kicked, but
if not you can come out of screen-blank with the same Surface you had before.

This raises a fundamental question when using a separate renderer thread with
SurfaceView: should the lifespan of the thread be tied to the Surface or to the
Activity? The answer is: it depends on what you want to have happen when the screen
goes blank. There are two basic approaches: (1) start/stop the thread on Activity
start/stop; (2) start/stop the thread on Surface create/destroy.

#1 interacts well with the app lifecycle. We start the renderer thread in onResume() and
stop it in onPause(). It gets a bit awkward when creating and configuring the thread
because sometimes the Surface will already exist and sometimes it won't. We can't simply
forward the Surface callbacks to the thread, because they won't fire again if the
Surface already exists. So we need to query or cache the Surface state, and forward it
to the renderer thread. Note we have to be a little careful here passing objects between
threads -- best to pass the Surface or SurfaceHolder through a Handler message, rather
than just stuffing it into the thread, to avoid issues on multi-core systems (cf.
Android SMP Primer).

#2 has a certain appeal because the Surface and the renderer are logically intertwined.
We start the thread after the Surface has been created, which avoids the inter-thread
communication concerns. Surface created / changed messages are simply forwarded. We
need to make sure rendering stops when the screen goes blank, and resumes when it
un-blanks; this could be a simple matter of telling Choreographer to stop invoking the
frame draw callback. Our onResume() will need to resume the callbacks if and only if
the renderer thread is running. It may not be so trivial though -- if we animate based
on elapsed time between frames, we could have a very large gap when the next event
arrives, so an explicit pause/resume message may be desirable.

The above is primarily concerned with how the renderer thread is configured and whether
it's executing. A related concern is extracting state from the thread when the
Activity is killed (in onPause() or onSaveInstanceState()). Approach #1 will work
best for that, because once the renderer thread has been joined its state can be
accessed without synchronization primitives.

Managing camera preview SurfaceView throughout the Activity lifecycle?

After a couple hours of trial and error, it seems that these lifecycle handlers work (i.e., they handle the pause caused by the power button, the stop caused by the home icon, the destroy caused by the back icon, and the camera is released onPause() so as to make it available for the system's camera application).

@Override
protected void onCreate(Bundle savedInstanceState) {
// Be sure to call the super class.
super.onCreate(savedInstanceState);

mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
mCamera = Camera.open(1);
mCameraPreview = new CameraPreview(this, mCamera);
mGraphView = new GraphView(this);
// setContentView(mGraphView);

// Create RelativeLayout for layout root.
mLayoutRoot = new RelativeLayout(this);
RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT,
RelativeLayout.LayoutParams.FILL_PARENT);

// Add GraphView to layout.
RelativeLayout.LayoutParams lpGraph = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT,
RelativeLayout.LayoutParams.FILL_PARENT);
mGraphView.setLayoutParams(lpGraph);
mLayoutRoot.addView(mGraphView);

// Add SurfaceView to layout.
List<Camera.Size> ls = mCamera.getParameters().getSupportedPreviewSizes();
int n = ls.size();
int widthMin = 10000;
int imin = -1;
for (int i=0; i<n; i++) {
Log.d(TAG, "supported preview width x height: " + ls.get(i).width + " x " + ls.get(i).height);
if (widthMin > ls.get(i).width) {
widthMin = ls.get(i).width;
mCameraPreviewSize = ls.get(i);
imin = i;
}
}
if (imin >= 0) {
RelativeLayout.LayoutParams lpSurface = new RelativeLayout.LayoutParams(
ls.get(imin).width, ls.get(imin).height);
lpSurface.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
lpSurface.addRule(RelativeLayout.CENTER_HORIZONTAL);
mCameraPreview.setLayoutParams(lpSurface);
mLayoutRoot.addView(mCameraPreview);
}

// Provide Android framework with layout root.
setContentView(mLayoutRoot, rlp);
Log.d(TAG, "onCreate OUT mCamera, mCameraPreview: " + mCamera + ", " + mCameraPreview);
}

@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart OUT mCamera, mCameraPreview: " + mCamera + ", " + mCameraPreview);
}

@Override
protected void onResume() {
super.onResume();
mSensorManager.registerListener(mGraphView,
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_FASTEST);
mSensorManager.registerListener(mGraphView,
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
SensorManager.SENSOR_DELAY_FASTEST);
mSensorManager.registerListener(mGraphView,
mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION),
SensorManager.SENSOR_DELAY_FASTEST);

if (mCamera == null)
mCamera = Camera.open(1);
if (mCameraPreview == null) {
mCameraPreview = new CameraPreview(this, mCamera);
RelativeLayout.LayoutParams lpCameraPreview = new RelativeLayout.LayoutParams(
mCameraPreviewSize.width, mCameraPreviewSize.height);
lpCameraPreview.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
lpCameraPreview.addRule(RelativeLayout.CENTER_HORIZONTAL);
mCameraPreview.setLayoutParams(lpCameraPreview);
mLayoutRoot.addView(mCameraPreview);
}
Log.d(TAG, "onResume OUT mCamera, mCameraPreview: " + mCamera + ", " + mCameraPreview);
}

@Override
protected void onPause() {
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release(); // release the camera for other applications
mCamera = null;
}
if (mCameraPreview != null) {
mLayoutRoot.removeView(mCameraPreview);
mCameraPreview = null;
}
super.onPause();
Log.d(TAG, "onPause OUT mCamera, mCameraPreview: " + mCamera + ", " + mCameraPreview);
}

@Override
protected void onStop() {
mSensorManager.unregisterListener(mGraphView);
super.onStop();
Log.d(TAG, "onStop OUT mCamera, mCameraPreview: " + mCamera + ", " + mCameraPreview);
}

@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy OUT mCamera, mCameraPreview: " + mCamera + ", " + mCameraPreview);
}

Android Game States Change when paused

Part of the reason for the behavior has to do with the way SurfaceView works. It's lifecycle is slightly independent of the usual Activity lifecycle.

The phenomenon is discussed in an appendix to the Graphics Architecture doc. In short, the system attempts to avoid destroying and re-creating the surface. If you rotate the device, you get pause/resume and destroy/create. If you just put the device to sleep and wake it up, and the lock screen doesn't force a rotation change, you will get pause/resume but no destroy/create.

The trick is to manage your rendering thread and resource allocation (notably stuff like opening and closing a Camera) in a way that fits with the dueling lifecycles.

There are two basic approaches: (1) start/stop the thread on Activity
start/stop; (2) start/stop the thread on Surface create/destroy.

The doc goes on to explain what each of those means, and when other actions must be taken in relation. Examples of both approaches can be found in Grafika; the "texture from camera" activity uses #1, while the "hardware scaler exerciser" activity uses #2.

Coming back around to your code, you're calling pauseApp() from surfaceDestroyed(). Since there's no guarantee that the surface will be destroyed, your pauseApp() code may not run when the app is paused. Add some logging to the various state-change functions and watch them fire (or not) in logcat as you navigate the device.

How can I re-use a surfaceHolder with camera.startPreview after I've already taken a picture?

Issue was ultimately not related to the capability of a surfaceHolder to be re-used as a camera preview on multiple photos in the same activity's lifetime. It was all down to my own logic that was checking whether the camera and holder were both indeed "ready".



Related Topics



Leave a reply



Submit