diff options
-rw-r--r-- | core/java/android/view/ThreadedRenderer.java | 6 | ||||
-rw-r--r-- | core/jni/android_view_ThreadedRenderer.cpp | 10 | ||||
-rw-r--r-- | graphics/java/android/graphics/PixelCopy.java | 104 | ||||
-rw-r--r-- | libs/hwui/Android.mk | 1 | ||||
-rw-r--r-- | libs/hwui/Readback.cpp | 159 | ||||
-rw-r--r-- | libs/hwui/Readback.h | 34 | ||||
-rw-r--r-- | libs/hwui/renderthread/RenderProxy.cpp | 15 | ||||
-rw-r--r-- | libs/hwui/renderthread/RenderProxy.h | 2 | ||||
-rw-r--r-- | tests/HwAccelerationTest/AndroidManifest.xml | 9 | ||||
-rw-r--r-- | tests/HwAccelerationTest/src/com/android/test/hwui/GetBitmapSurfaceViewActivity.java | 111 | ||||
-rw-r--r-- | tests/HwAccelerationTest/src/com/android/test/hwui/HardwareCanvasSurfaceViewActivity.java | 57 |
11 files changed, 503 insertions, 5 deletions
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index f44d4c1ac9df..206ba1664407 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -903,6 +903,10 @@ public final class ThreadedRenderer { nSerializeDisplayListTree(mNativeProxy); } + public static boolean copySurfaceInto(Surface surface, Bitmap bitmap) { + return nCopySurfaceInto(surface, bitmap); + } + @Override protected void finalize() throws Throwable { try { @@ -1040,4 +1044,6 @@ public final class ThreadedRenderer { private static native long nAddFrameMetricsObserver(long nativeProxy, FrameMetricsObserver observer); private static native void nRemoveFrameMetricsObserver(long nativeProxy, long nativeObserver); + + private static native boolean nCopySurfaceInto(Surface surface, Bitmap bitmap); } diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp index ef45c87277ae..68c818e92e5c 100644 --- a/core/jni/android_view_ThreadedRenderer.cpp +++ b/core/jni/android_view_ThreadedRenderer.cpp @@ -669,6 +669,14 @@ static void android_view_ThreadedRenderer_setContentDrawBounds(JNIEnv* env, proxy->setContentDrawBounds(left, top, right, bottom); } +static jboolean android_view_ThreadedRenderer_copySurfaceInto(JNIEnv* env, + jobject clazz, jobject jsurface, jobject jbitmap) { + SkBitmap bitmap; + GraphicsJNI::getSkBitmap(env, jbitmap, &bitmap); + sp<Surface> surface = android_view_Surface_getSurface(env, jsurface); + return RenderProxy::copySurfaceInto(surface, &bitmap); +} + // ---------------------------------------------------------------------------- // FrameMetricsObserver // ---------------------------------------------------------------------------- @@ -775,6 +783,8 @@ static const JNINativeMethod gMethods[] = { { "nRemoveFrameMetricsObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_removeFrameMetricsObserver }, + { "nCopySurfaceInto", "(Landroid/view/Surface;Landroid/graphics/Bitmap;)Z", + (void*)android_view_ThreadedRenderer_copySurfaceInto }, }; int register_android_view_ThreadedRenderer(JNIEnv* env) { diff --git a/graphics/java/android/graphics/PixelCopy.java b/graphics/java/android/graphics/PixelCopy.java new file mode 100644 index 000000000000..c5991264e555 --- /dev/null +++ b/graphics/java/android/graphics/PixelCopy.java @@ -0,0 +1,104 @@ +package android.graphics; + +import android.annotation.NonNull; +import android.os.Handler; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.ThreadedRenderer; + +/** + * Provides a mechanisms to issue pixel copy requests to allow for copy + * operations from {@link Surface} to {@link Bitmap} + * + * @hide + */ +public final class PixelCopy { + /** + * Contains the result of a pixel copy request + */ + public static final class Response { + /** + * Indicates whether or not the copy request completed successfully. + * If this is true, then {@link #bitmap} contains the result of the copy. + * If this is false, {@link #bitmap} is unmodified from the originally + * passed destination. + * + * For example a request might fail if the source is protected content + * so copies are not allowed. Similarly if the source has nothing to + * copy from, because either no frames have been produced yet or because + * it has already been destroyed, then this will be false. + */ + public boolean success; + + /** + * The output bitmap. This is always the same object that was passed + * to request() as the 'dest' bitmap. If {@link #success} is true this + * contains a copy of the pixels of the source object. If {@link #success} + * is false then this is unmodified. + */ + @NonNull + public Bitmap bitmap; + } + + public interface OnPixelCopyFinished { + /** + * Callback for when a pixel copy request has completed. This will be called + * regardless of whether the copy succeeded or failed. + * + * @param response Contains the result of the copy request which includes + * whether or not the copy was successful. + */ + void onPixelCopyFinished(PixelCopy.Response response); + } + + /** + * Requests for the display content of a {@link SurfaceView} to be copied + * into a provided {@link Bitmap}. + * + * The contents of the source will be scaled to fit exactly inside the bitmap. + * The pixel format of the source buffer will be converted, as part of the copy, + * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * in the SurfaceView's Surface will be used as the source of the copy. + * + * @param source The source from which to copy + * @param dest The destination of the copy. The source will be scaled to + * match the width, height, and format of this bitmap. + * @param listener Callback for when the pixel copy request completes + * @param listenerThread The callback will be invoked on this Handler when + * the copy is finished. + */ + public static void request(@NonNull SurfaceView source, @NonNull Bitmap dest, + @NonNull OnPixelCopyFinished listener, @NonNull Handler listenerThread) { + request(source.getHolder().getSurface(), dest, listener, listenerThread); + } + + /** + * Requests a copy of the pixels from a {@link Surface} to be copied into + * a provided {@link Bitmap}. + * + * The contents of the source will be scaled to fit exactly inside the bitmap. + * The pixel format of the source buffer will be converted, as part of the copy, + * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * in the Surface will be used as the source of the copy. + * + * @param source The source from which to copy + * @param dest The destination of the copy. The source will be scaled to + * match the width, height, and format of this bitmap. + * @param listener Callback for when the pixel copy request completes + * @param listenerThread The callback will be invoked on this Handler when + * the copy is finished. + */ + public static void request(@NonNull Surface source, @NonNull Bitmap dest, + @NonNull OnPixelCopyFinished listener, @NonNull Handler listenerThread) { + // TODO: Make this actually async and fast and cool and stuff + final PixelCopy.Response response = new PixelCopy.Response(); + response.success = ThreadedRenderer.copySurfaceInto(source, dest); + response.bitmap = dest; + listenerThread.post(new Runnable() { + @Override + public void run() { + listener.onPixelCopyFinished(response); + } + }); + } +} diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk index 0606b0b1a158..717a1e6eacc4 100644 --- a/libs/hwui/Android.mk +++ b/libs/hwui/Android.mk @@ -84,6 +84,7 @@ hwui_src_files := \ Properties.cpp \ PropertyValuesHolder.cpp \ PropertyValuesAnimatorSet.cpp \ + Readback.cpp \ RenderBufferCache.cpp \ RenderNode.cpp \ RenderProperties.cpp \ diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp new file mode 100644 index 000000000000..d7df77c2f3ed --- /dev/null +++ b/libs/hwui/Readback.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Readback.h" + +#include "Caches.h" +#include "Image.h" +#include "GlopBuilder.h" +#include "renderstate/RenderState.h" +#include "renderthread/EglManager.h" +#include "utils/GLUtils.h" + +#include <GLES2/gl2.h> +#include <ui/Fence.h> +#include <ui/GraphicBuffer.h> + +namespace android { +namespace uirenderer { + +bool Readback::copySurfaceInto(renderthread::RenderThread& renderThread, + Surface& surface, SkBitmap* bitmap) { + // TODO: Clean this up and unify it with LayerRenderer::copyLayer, + // of which most of this is copied from. + renderThread.eglManager().initialize(); + + Caches& caches = Caches::getInstance(); + RenderState& renderState = renderThread.renderState(); + int destWidth = bitmap->width(); + int destHeight = bitmap->height(); + if (destWidth > caches.maxTextureSize + || destHeight > caches.maxTextureSize) { + ALOGW("Can't copy surface into bitmap, %dx%d exceeds max texture size %d", + destWidth, destHeight, caches.maxTextureSize); + return false; + } + GLuint fbo = renderState.createFramebuffer(); + if (!fbo) { + ALOGW("Could not obtain an FBO"); + return false; + } + + SkAutoLockPixels alp(*bitmap); + + GLuint texture; + + GLenum format; + GLenum type; + + switch (bitmap->colorType()) { + case kAlpha_8_SkColorType: + format = GL_ALPHA; + type = GL_UNSIGNED_BYTE; + break; + case kRGB_565_SkColorType: + format = GL_RGB; + type = GL_UNSIGNED_SHORT_5_6_5; + break; + case kARGB_4444_SkColorType: + format = GL_RGBA; + type = GL_UNSIGNED_SHORT_4_4_4_4; + break; + case kN32_SkColorType: + default: + format = GL_RGBA; + type = GL_UNSIGNED_BYTE; + break; + } + + renderState.bindFramebuffer(fbo); + + // TODO: Use layerPool or something to get this maybe? But since we + // need explicit format control we can't currently. + + // Setup the rendertarget + glGenTextures(1, &texture); + caches.textureState().activateTexture(0); + caches.textureState().bindTexture(texture); + glPixelStorei(GL_PACK_ALIGNMENT, bitmap->bytesPerPixel()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, format, destWidth, destHeight, + 0, format, type, nullptr); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, texture, 0); + + // Setup the source + sp<GraphicBuffer> sourceBuffer; + sp<Fence> sourceFence; + // FIXME: Waiting on an API from libgui for this + // surface.getLastQueuedBuffer(&sourceBuffer, &sourceFence); + if (!sourceBuffer.get()) { + ALOGW("Surface doesn't have any previously queued frames, nothing to readback from"); + return false; + } + int err = sourceFence->wait(500 /* ms */); + if (err != NO_ERROR) { + ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt"); + return false; + } + Image sourceImage(sourceBuffer); + if (!sourceImage.getTexture()) { + ALOGW("Failed to make an EGLImage from the GraphicBuffer"); + return false; + } + Texture sourceTexture(caches); + sourceTexture.wrap(sourceImage.getTexture(), + sourceBuffer->getWidth(), sourceBuffer->getHeight(), 0 /* total lie */); + + { + // Draw & readback + renderState.setViewport(destWidth, destHeight); + renderState.scissor().setEnabled(false); + renderState.blend().syncEnabled(); + renderState.stencil().disable(); + + Rect destRect(destWidth, destHeight); + Glop glop; + GlopBuilder(renderState, caches, &glop) + .setRoundRectClipState(nullptr) + .setMeshTexturedUvQuad(nullptr, Rect(0, 1, 1, 0)) // TODO: simplify with VBO + .setFillLayer(sourceTexture, nullptr, 1.0f, SkXfermode::kSrc_Mode, + Blend::ModeOrderSwap::NoSwap) + .setTransform(Matrix4::identity(), TransformFlags::None) + .setModelViewMapUnitToRect(destRect) + .build(); + Matrix4 ortho; + ortho.loadOrtho(destWidth, destHeight); + renderState.render(glop, ortho); + + glReadPixels(0, 0, bitmap->width(), bitmap->height(), format, + type, bitmap->getPixels()); + } + + // Cleanup + caches.textureState().deleteTexture(texture); + renderState.deleteFramebuffer(fbo); + + GL_CHECKPOINT(MODERATE); + + return true; +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/Readback.h b/libs/hwui/Readback.h new file mode 100644 index 000000000000..ea03c829f492 --- /dev/null +++ b/libs/hwui/Readback.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "renderthread/RenderThread.h" + +#include <SkBitmap.h> +#include <gui/Surface.h> + +namespace android { +namespace uirenderer { + +class Readback { +public: + static bool copySurfaceInto(renderthread::RenderThread& renderThread, + Surface& surface, SkBitmap* bitmap); +}; + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index c5a2dc7bd8ce..5e37856ecce9 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -19,6 +19,7 @@ #include "DeferredLayerUpdater.h" #include "DisplayList.h" #include "LayerRenderer.h" +#include "Readback.h" #include "Rect.h" #include "renderthread/CanvasContext.h" #include "renderthread/RenderTask.h" @@ -616,6 +617,20 @@ void RenderProxy::removeFrameMetricsObserver(FrameMetricsObserver* observer) { post(task); } +CREATE_BRIDGE3(copySurfaceInto, RenderThread* thread, + Surface* surface, SkBitmap* bitmap) { + return (void*) Readback::copySurfaceInto(*args->thread, + *args->surface, args->bitmap); +} + +bool RenderProxy::copySurfaceInto(sp<Surface>& surface, SkBitmap* bitmap) { + SETUP_TASK(copySurfaceInto); + args->bitmap = bitmap; + args->surface = surface.get(); + args->thread = &RenderThread::getInstance(); + return (bool) staticPostAndWait(task); +} + void RenderProxy::post(RenderTask* task) { mRenderThread.queue(task); } diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index 32d3283bd814..c39319da4b39 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -127,6 +127,8 @@ public: ANDROID_API void removeFrameMetricsObserver(FrameMetricsObserver* observer); ANDROID_API long getDroppedFrameReportCount(); + ANDROID_API static bool copySurfaceInto(sp<Surface>& surface, SkBitmap* bitmap); + private: RenderThread& mRenderThread; CanvasContext* mContext; diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml index 18fd98528855..b9e9ac856185 100644 --- a/tests/HwAccelerationTest/AndroidManifest.xml +++ b/tests/HwAccelerationTest/AndroidManifest.xml @@ -374,6 +374,15 @@ </activity> <activity + android:name="GetBitmapSurfaceViewActivity" + android:label="SurfaceView/GetBitmap with Camera source"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="com.android.test.hwui.TEST" /> + </intent-filter> + </activity> + + <activity android:name="GLTextureViewActivity" android:label="TextureView/OpenGL"> <intent-filter> diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/GetBitmapSurfaceViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/GetBitmapSurfaceViewActivity.java new file mode 100644 index 000000000000..d3cd7db7d46a --- /dev/null +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/GetBitmapSurfaceViewActivity.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.hwui; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.PixelCopy; +import android.graphics.PixelCopy.OnPixelCopyFinished; +import android.graphics.PixelCopy.Response; +import android.hardware.Camera; +import android.os.Bundle; +import android.os.Environment; +import android.view.Gravity; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.Toast; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +public class GetBitmapSurfaceViewActivity extends Activity implements SurfaceHolder.Callback { + private Camera mCamera; + private SurfaceView mSurfaceView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + FrameLayout content = new FrameLayout(this); + + mSurfaceView = new SurfaceView(this); + mSurfaceView.getHolder().addCallback(this); + + Button button = new Button(this); + button.setText("Copy bitmap to /sdcard/surfaceview.png"); + button.setOnClickListener((View v) -> { + Bitmap b = Bitmap.createBitmap( + mSurfaceView.getWidth(), + mSurfaceView.getHeight(), + Bitmap.Config.ARGB_8888); + PixelCopy.request(mSurfaceView, b, + mOnCopyFinished, mSurfaceView.getHandler()); + }); + + content.addView(mSurfaceView, new FrameLayout.LayoutParams(500, 400, Gravity.CENTER)); + content.addView(button, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM)); + setContentView(content); + } + + private final OnPixelCopyFinished mOnCopyFinished = new OnPixelCopyFinished() { + @Override + public void onPixelCopyFinished(Response response) { + if (!response.success) { + Toast.makeText(GetBitmapSurfaceViewActivity.this, + "Failed to copy", Toast.LENGTH_SHORT).show(); + return; + } + try { + try (FileOutputStream out = new FileOutputStream( + Environment.getExternalStorageDirectory() + "/surfaceview.png");) { + response.bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + } + } catch (Exception e) { + // Ignore + } + } + }; + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mCamera = Camera.open(); + + try { + mCamera.setPreviewSurface(holder.getSurface()); + } catch (IOException t) { + android.util.Log.e("TextureView", "Cannot set preview texture target!", t); + } + + mCamera.startPreview(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mCamera.stopPreview(); + mCamera.release(); + } +} diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/HardwareCanvasSurfaceViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/HardwareCanvasSurfaceViewActivity.java index b1431c586631..5c30faba3d18 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/HardwareCanvasSurfaceViewActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/HardwareCanvasSurfaceViewActivity.java @@ -17,18 +17,29 @@ package com.android.test.hwui; import android.app.Activity; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.PixelCopy; +import android.graphics.PixelCopy.OnPixelCopyFinished; +import android.graphics.PixelCopy.Response; import android.graphics.PorterDuff; import android.os.Bundle; -import android.view.Gravity; +import android.os.Environment; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; +import android.view.View; +import android.widget.Button; import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Toast; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; -@SuppressWarnings({"UnusedDeclaration"}) public class HardwareCanvasSurfaceViewActivity extends Activity implements Callback { private SurfaceView mSurfaceView; private HardwareCanvasSurfaceViewActivity.RenderingThread mThread; @@ -42,13 +53,49 @@ public class HardwareCanvasSurfaceViewActivity extends Activity implements Callb mSurfaceView = new SurfaceView(this); mSurfaceView.getHolder().addCallback(this); - content.addView(mSurfaceView, new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, + Button button = new Button(this); + button.setText("Copy bitmap to /sdcard/surfaceview.png"); + button.setOnClickListener((View v) -> { + Bitmap b = Bitmap.createBitmap( + mSurfaceView.getWidth(), + mSurfaceView.getHeight(), + Bitmap.Config.ARGB_8888); + PixelCopy.request(mSurfaceView, b, + mOnCopyFinished, mSurfaceView.getHandler()); + }); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.addView(button, LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + layout.addView(mSurfaceView, LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT); + + content.addView(layout, new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, - Gravity.CENTER)); + FrameLayout.LayoutParams.MATCH_PARENT)); setContentView(content); } + private final OnPixelCopyFinished mOnCopyFinished = new OnPixelCopyFinished() { + @Override + public void onPixelCopyFinished(Response response) { + if (!response.success) { + Toast.makeText(HardwareCanvasSurfaceViewActivity.this, + "Failed to copy", Toast.LENGTH_SHORT).show(); + return; + } + try { + try (FileOutputStream out = new FileOutputStream( + Environment.getExternalStorageDirectory() + "/surfaceview.png");) { + response.bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + } + } catch (Exception e) { + // Ignore + } + } + }; + @Override public void surfaceCreated(SurfaceHolder holder) { mThread = new RenderingThread(holder.getSurface()); |