So I have a problem trying to capture my application’s screen in Android. Here’s the situation:
I would like to continuously capture the application screen using the View.getDrawingCache() method and detect which part of the screen has changed. My application is based on a single TabHost with 5 activities in it and I would like to capture everything that is happening in the tabhost. I also want the screen capturing code to run in the background and for this purpose I have created an AsyncTask instance to capture the image and check if something has changed. The problem is that the getDrawingCache() method sometimes causes the app to crash raising the CalledFromWrongThreadException.
I have done some testing and here’s what I found.. First of all I am not even changing between the activities in the tabhost so only the first one is visible. The first activity has a ScrollView and as soon as I disable the scrollbars with “android:scrollbars=’none'” the capturing works but if I reenable them it crashes with the same exception. I think the problem is that I am doing the screen capture in the doInBackground method and Android isn’t liking that for some reason (well, I know the reason – it’s trying to draw the cache in a thread other than the main one…).
So my question is does anyone have an idea how I can achieve recording in the background?
Here’s the main parts of the code to make things clear…
public class TRCTargetMainActivity extends TabActivity {
private static final Logger tracelog = Logger.getLogger( TRCTargetMainActivity.class.getName() );
private boolean isVisible;
private int selectedIndex;
private Bitmap latestScreen, currentScreen;
private boolean stop = false;
private ScreenCaptureThread scThread;
private TabHost mTabHost;
private MyNotificationReceiver receiver;
private BadgeView chatBadge, transfersBadge;
public void onCreate( Bundle savedInstanceState ) {
super.onCreate(savedInstanceState);
requestWindowFeature( Window.FEATURE_NO_TITLE );
setContentView( R.layout.activity_main );
mTabHost = getTabHost();
buildTab( "deviceSpec", getString( R.string.activity_trctargetmain_title_info ), R.drawable.ic_tab_device_info, DeviceInfoActivity.class );
buildTab( "fileExplorerSpec", getString( R.string.activity_trctargetmain_title_transferfolder ), R.drawable.ic_tab_transfer, FileExplorerActivity.class );
buildTab( "chatSpec", getString( R.string.activity_trctargetmain_title_chat ), R.drawable.ic_tab_chat, ChatActivity.class );
buildTab( "transfersSpec", getString( R.string.activity_trctargetmain_title_transfers ), R.drawable.ic_tab_transfers, TransfersActivity.class );
buildTab( "settingsSpec", getString( R.string.activity_trctargetmain_title_settings ), R.drawable.ic_tab_settings, SettingsActivity.class );
Bundle extras = getIntent().getExtras();
if ( extras != null ) {
selectedIndex = extras.getInt( Settings.EXTRA_SELECTED_TAB );
mTabHost.setCurrentTab( selectedIndex );
}
mTabHost.setOnTabChangedListener( new OnTabChangeListener() {
public void onTabChanged( String arg0 ) {
selectedIndex = mTabHost.getCurrentTab();
}
} );
chatBadge = new BadgeView( this, mTabHost.getTabWidget(), 2 );
transfersBadge = new BadgeView( this, mTabHost.getTabWidget(), 3 );
initializeBroadcastReceiver();
configureTabHostBackground();
checkDirectoryExistence();
checkTargetServiceState();
doStartScreenCapture();
}
...
public void doStartScreenCapture() {
stop = false;
if ( scThread == null ) {
scThread = new ScreenCaptureThread();
scThread.execute();
}
}
...
private Bitmap getScreenBitmap() {
RelativeLayout r = (RelativeLayout) this.findViewById( R.id.rootLayout );
View v = r.getRootView();
v.setDrawingCacheEnabled( true );
Bitmap bm = v.getDrawingCache();
if ( bm == null )
return bm;
int[] pixels = new int[ bm.getWidth() * bm.getHeight() ];
bm.getPixels( pixels, 0, bm.getWidth(), 0, 0, bm.getWidth(), bm.getHeight() );
return Bitmap.createBitmap( pixels, bm.getWidth(), bm.getHeight(), Bitmap.Config.ARGB_8888 );
}
...
private class ScreenCaptureThread extends AsyncTask<String,Integer,Boolean> {
private void sendScreenUpdate( int[] data, int row, int col ) {
ByteBuffer byteBuffer = ByteBuffer.allocate( data.length * 4 );
IntBuffer intBuffer = byteBuffer.asIntBuffer();
intBuffer.put( data );
byte[] array = byteBuffer.array();
// send the screen update
}
private boolean checkForScreenChanges() {
int[] pixels1 = new int[ Settings.SCREEN_BLOCK_SIZE * Settings.SCREEN_BLOCK_SIZE ];
int[] pixels2 = new int[ Settings.SCREEN_BLOCK_SIZE * Settings.SCREEN_BLOCK_SIZE ];
int numCols = currentScreen.getWidth() / Settings.SCREEN_BLOCK_SIZE;
int numRows = currentScreen.getHeight() / Settings.SCREEN_BLOCK_SIZE;
int x = 0;
int y = 0;
boolean differenceFound = false;
for ( int i = 0; i < numRows; i++ ) {
for ( int j = 0; j < numCols; j++ ) {
latestScreen.getPixels( pixels1, 0, Settings.SCREEN_BLOCK_SIZE, x, y, Settings.SCREEN_BLOCK_SIZE, Settings.SCREEN_BLOCK_SIZE );
currentScreen.getPixels( pixels2, 0, Settings.SCREEN_BLOCK_SIZE, x, y, Settings.SCREEN_BLOCK_SIZE, Settings.SCREEN_BLOCK_SIZE );
if ( !Arrays.equals( pixels1, pixels2 ) ) {
sendScreenUpdate( pixels2, i, j );
differenceFound = true;
}
Arrays.fill( pixels1, 0 );
Arrays.fill( pixels2, 0 );
x += Settings.SCREEN_BLOCK_SIZE;
}
x = 0;
y += Settings.SCREEN_BLOCK_SIZE;
}
return differenceFound;
}
@Override
protected Boolean doInBackground( String... arg0 ) {
System.out.println( "SCREEN CAPTURE THREAD STARED" );
currentScreen = getScreenBitmap();
latestScreen = getScreenBitmap();
while ( !stop ) {
try {
Thread.sleep( 3000 );
} catch ( Exception e ) {}
if ( latestScreen != null && currentScreen != null ) {
if ( checkForScreenChanges() ) {
latestScreen = getScreenBitmap();
System.out.println( "SCREEN UPDATED" );
}
}
if ( currentScreen != null && !currentScreen.isRecycled() )
currentScreen.recycle();
currentScreen = getScreenBitmap();
}
return true;
}
@Override
protected void onPostExecute( Boolean result ) {
System.out.println( "SCREEN CAPTURE THREAD STOPPED" );
}
}
}
and here’s the activity’s xml file…
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/rootLayout"
>
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="@android:id/tabs">
<RelativeLayout android:id="@+id/emptylayout1" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"/>
<RelativeLayout android:id="@+id/emptylayout2" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"/>
<RelativeLayout android:id="@+id/emptylayout3" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"/>
</FrameLayout>
<TabWidget
android:id="@android:id/tabs"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="-4dip" />
</RelativeLayout>
</TabHost>
Finally, here’s the exception that is thrown…
01-21 18:11:40.070: E/AndroidRuntime(11279): FATAL EXCEPTION: AsyncTask #1
01-21 18:11:40.070: E/AndroidRuntime(11279): java.lang.RuntimeException: An error occured while executing doInBackground()
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.os.AsyncTask$3.done(AsyncTask.java:200)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:274)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.util.concurrent.FutureTask.setException(FutureTask.java:125)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:308)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.util.concurrent.FutureTask.run(FutureTask.java:138)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1088)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:581)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.lang.Thread.run(Thread.java:1019)
01-21 18:11:40.070: E/AndroidRuntime(11279): Caused by: android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewRoot.checkThread(ViewRoot.java:3055)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewRoot.invalidateChild(ViewRoot.java:657)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewRoot.invalidateChildInParent(ViewRoot.java:683)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.invalidateChild(ViewGroup.java:2514)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.invalidate(View.java:5479)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.awakenScrollBars(View.java:5372)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.awakenScrollBars(View.java:5264)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.widget.ScrollView.onOverScrolled(ScrollView.java:820)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.overScrollBy(View.java:9100)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.widget.ScrollView.computeScrollBounce(ScrollView.java:1325)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.widget.ScrollView.computeScroll(ScrollView.java:1366)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1562)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.draw(View.java:7083)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1646)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.draw(View.java:7083)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.widget.FrameLayout.draw(FrameLayout.java:357)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1646)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1644)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1644)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1644)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1644)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.draw(View.java:7083)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.widget.FrameLayout.draw(FrameLayout.java:357)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.drawChild(ViewGroup.java:1646)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.draw(View.java:7083)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.widget.FrameLayout.draw(FrameLayout.java:357)
01-21 18:11:40.070: E/AndroidRuntime(11279): at com.android.internal.policy.impl.PhoneWindow$DecorView.draw(PhoneWindow.java:2119)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.buildDrawingCache(View.java:6842)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.getDrawingCache(View.java:6628)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.view.View.getDrawingCache(View.java:6593)
01-21 18:11:40.070: E/AndroidRuntime(11279): at com.ibm.trctarget.TRCTargetMainActivity.getScreenBitmap(TRCTargetMainActivity.java:158)
01-21 18:11:40.070: E/AndroidRuntime(11279): at com.ibm.trctarget.TRCTargetMainActivity.access$6(TRCTargetMainActivity.java:153)
01-21 18:11:40.070: E/AndroidRuntime(11279): at com.ibm.trctarget.TRCTargetMainActivity$ScreenCaptureThread.doInBackground(TRCTargetMainActivity.java:391)
01-21 18:11:40.070: E/AndroidRuntime(11279): at com.ibm.trctarget.TRCTargetMainActivity$ScreenCaptureThread.doInBackground(TRCTargetMainActivity.java:1)
01-21 18:11:40.070: E/AndroidRuntime(11279): at android.os.AsyncTask$2.call(AsyncTask.java:185)
01-21 18:11:40.070: E/AndroidRuntime(11279): at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:306)
01-21 18:11:40.070: E/AndroidRuntime(11279): ... 4 more
any help is very appreciated 🙂
The dead stupid and simple way would be to wrap things that need to run on the main thread in
runOnUiThread(...)