diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index a7849b6ed6..e24c30f36e 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -1201,6 +1201,19 @@ public Image flipImageVertically(Image image, boolean maintainOpacity) { return EncodedImage.createFromRGB(newRGB, width, height, !maintainOpacity); } + /** + * Tries to grab an OS native screenshot which would include peer components etc. + * On fallback draws the current Form object. + * + * @param callback invoked with the screenshot + */ + public void screenshot(SuccessCallback callback) { + Form current = getCurrentForm(); + Image img = Image.createImage(current.getWidth(), current.getHeight()); + current.paintComponent(img.getGraphics(), true); + callback.onSucess(img); + } + /** * Returns true if the platform supports a native image cache. The native image cache * is different than just {@link FileSystemStorage#hasCachesDir()}. A native image cache @@ -5686,6 +5699,7 @@ public void capturePhoto(ActionListener response) { * * @return An image of the screen, or null if it failed. * @since 7.0 + * @deprecated replaced by screenshot() */ public Image captureScreen() { Form form = getCurrentForm(); diff --git a/CodenameOne/src/com/codename1/testing/TestUtils.java b/CodenameOne/src/com/codename1/testing/TestUtils.java index 6a505f3689..866fd9b6d0 100644 --- a/CodenameOne/src/com/codename1/testing/TestUtils.java +++ b/CodenameOne/src/com/codename1/testing/TestUtils.java @@ -584,7 +584,8 @@ public static boolean screenshotTest(String screenshotName) { return true; } - Image mute = Image.createImage(Display.getInstance().getDisplayWidth(), Display.getInstance().getDisplayHeight()); + + Image mute = Display.getInstance().captureScreen(); Display.getInstance().getCurrent().paintComponent(mute.getGraphics(), true); screenshotName = screenshotName + ".png"; if (Storage.getInstance().exists(screenshotName)) { diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index ec59c162dd..09531b029e 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -62,6 +62,7 @@ import com.codename1.ui.util.ImageIO; import com.codename1.util.AsyncResource; import com.codename1.util.RunnableWithResultSync; +import com.codename1.util.SuccessCallback; import java.io.IOException; import java.io.InputStream; @@ -4995,12 +4996,25 @@ public void onCanInstallOnHomescreen(Runnable r) { * * @return An image of the screen, or null if it failed. * @since 7.0 + * @deprecated use screenshot(SuccessCallback) instead */ public Image captureScreen() { return impl.captureScreen(); } /** + * Captures a screenshot in the native layer which should include peer + * components as well. + * + * @param callback will be invoked on the EDT with a screenshot + * @since 7.0.211 + */ + public void screenshot(SuccessCallback callback) { + impl.screenshot(callback); + } + + + /** * Convenience method to schedule a task to run on the EDT after {@literal timeout}ms. * * @param timeout The timeout in milliseconds. diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index 279a6517ce..5bbea626e9 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -57,9 +57,9 @@ import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -114,10 +114,10 @@ import android.telephony.SmsManager; import android.telephony.gsm.GsmCellLocation; import android.text.Html; -import android.view.*; -import android.view.View.MeasureSpec; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; +import android.view.*; +import android.view.View.MeasureSpec; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.webkit.*; import android.widget.*; import com.codename1.background.BackgroundFetch; @@ -375,6 +375,13 @@ public static void stopContext(Context ctx) { } } + @Override + public void screenshot(SuccessCallback callback) { + final Activity activity = (Activity) getContext(); + final AndroidScreenshotTask task = new AndroidScreenshotTask(myView, activity, callback); + activity.runOnUiThread(task); + } + @Override public void setPlatformHint(String key, String value) { if(key.equals("platformHint.compatPaintMode")) { @@ -8006,27 +8013,27 @@ private String getImageFilePath(Uri uri) { //String[] filePaths = file.getPath().split(":"); //String image_id = filePath[filePath.length - 1]; String[] filePathColumn = {MediaStore.Images.Media.DATA}; - Cursor cursor = getContext().getContentResolver().query( - android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[]{ MediaStore.Images.Media.DATA}, - null, - null, - null - ); - // Some gallery providers may return an empty cursor on modern Android builds. - String filePath = null; - if (cursor != null) { - try { - int columnIndex = cursor.getColumnIndex(filePathColumn[0]); - if (columnIndex >= 0 && cursor.moveToFirst()) { - filePath = cursor.getString(columnIndex); - } - } finally { - cursor.close(); - } - } - - if (filePath == null || "content".equals(scheme)) { + Cursor cursor = getContext().getContentResolver().query( + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[]{ MediaStore.Images.Media.DATA}, + null, + null, + null + ); + // Some gallery providers may return an empty cursor on modern Android builds. + String filePath = null; + if (cursor != null) { + try { + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + if (columnIndex >= 0 && cursor.moveToFirst()) { + filePath = cursor.getString(columnIndex); + } + } finally { + cursor.close(); + } + } + + if (filePath == null || "content".equals(scheme)) { //if the file is not on the filesystem download it and save it //locally try { @@ -8167,26 +8174,26 @@ else if (requestCode == FILECHOOSER_RESULTCODE) { Uri selectedImage = intent.getData(); String scheme = intent.getScheme(); - String[] filePathColumn = {MediaStore.Images.Media.DATA}; - Cursor cursor = getContext().getContentResolver().query(selectedImage, filePathColumn, null, null, null); - - // Some gallery providers may return an empty cursor on modern Android builds. - String filePath = null; - if (cursor != null) { - try { - int columnIndex = cursor.getColumnIndex(filePathColumn[0]); - if (columnIndex >= 0 && cursor.moveToFirst()) { - filePath = cursor.getString(columnIndex); - } - } finally { - cursor.close(); - } - } - boolean fileExists = false; - if (filePath != null) { - File file = new File(filePath); - fileExists = file.exists() && file.canRead(); - } + String[] filePathColumn = {MediaStore.Images.Media.DATA}; + Cursor cursor = getContext().getContentResolver().query(selectedImage, filePathColumn, null, null, null); + + // Some gallery providers may return an empty cursor on modern Android builds. + String filePath = null; + if (cursor != null) { + try { + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + if (columnIndex >= 0 && cursor.moveToFirst()) { + filePath = cursor.getString(columnIndex); + } + } finally { + cursor.close(); + } + } + boolean fileExists = false; + if (filePath != null) { + File file = new File(filePath); + fileExists = file.exists() && file.canRead(); + } if (!fileExists && "content".equals(scheme)) { //if the file is not on the filesystem download it and save it @@ -8214,23 +8221,23 @@ else if (requestCode == FILECHOOSER_RESULTCODE) { } } - if (filePath == null) { - callback.fireActionEvent(null); - return; - } - - callback.fireActionEvent(new ActionEvent(new String[]{filePath})); - return; + if (filePath == null) { + callback.fireActionEvent(null); + return; + } + + callback.fireActionEvent(new ActionEvent(new String[]{filePath})); + return; } else if (requestCode == OPEN_GALLERY) { Uri selectedImage = intent.getData(); String scheme = intent.getScheme(); - String[] filePathColumn = {MediaStore.Images.Media.DATA}; - Cursor cursor = getContext().getContentResolver().query(selectedImage, filePathColumn, null, null, null); - - // Some gallery providers may return an empty cursor on modern Android builds. - String filePath = null; + String[] filePathColumn = {MediaStore.Images.Media.DATA}; + Cursor cursor = getContext().getContentResolver().query(selectedImage, filePathColumn, null, null, null); + + // Some gallery providers may return an empty cursor on modern Android builds. + String filePath = null; if (cursor != null) { try { int columnIndex = cursor.getColumnIndex(filePathColumn[0]); @@ -10818,50 +10825,50 @@ public void run() { return true; } - public boolean isJailbrokenDevice() { - try { - Runtime.getRuntime().exec("su"); - return true; - } catch(Throwable t) { - com.codename1.io.Log.e(t); - } - return false; - } - - @Override - public void announceForAccessibility(final Component cmp, final String text) { - final Activity act = getActivity(); - if (act == null) { - return; - } - act.runOnUiThread(new Runnable() { - @Override - public void run() { - View view = null; - if (cmp instanceof PeerComponent) { - Object peer = ((PeerComponent) cmp).getNativePeer(); - if (peer instanceof View) { - view = (View) peer; - } - } - if (view == null) { - view = act.getWindow().getDecorView(); - } - if (view == null) { - return; - } - if (Build.VERSION.SDK_INT >= 16) { - view.announceForAccessibility(text); - } else { - AccessibilityManager manager = (AccessibilityManager) act.getSystemService(Context.ACCESSIBILITY_SERVICE); - if (manager != null && manager.isEnabled()) { - AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); - event.getText().add(text); - event.setSource(view); - manager.sendAccessibilityEvent(event); - } - } - } - }); - } -} + public boolean isJailbrokenDevice() { + try { + Runtime.getRuntime().exec("su"); + return true; + } catch(Throwable t) { + com.codename1.io.Log.e(t); + } + return false; + } + + @Override + public void announceForAccessibility(final Component cmp, final String text) { + final Activity act = getActivity(); + if (act == null) { + return; + } + act.runOnUiThread(new Runnable() { + @Override + public void run() { + View view = null; + if (cmp instanceof PeerComponent) { + Object peer = ((PeerComponent) cmp).getNativePeer(); + if (peer instanceof View) { + view = (View) peer; + } + } + if (view == null) { + view = act.getWindow().getDecorView(); + } + if (view == null) { + return; + } + if (Build.VERSION.SDK_INT >= 16) { + view.announceForAccessibility(text); + } else { + AccessibilityManager manager = (AccessibilityManager) act.getSystemService(Context.ACCESSIBILITY_SERVICE); + if (manager != null && manager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); + event.getText().add(text); + event.setSource(view); + manager.sendAccessibilityEvent(event); + } + } + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidScreenshotTask.java b/Ports/Android/src/com/codename1/impl/android/AndroidScreenshotTask.java new file mode 100644 index 0000000000..add1ea093e --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidScreenshotTask.java @@ -0,0 +1,108 @@ +package com.codename1.impl.android; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.view.PixelCopy; +import android.view.View; + +import com.codename1.impl.android.AndroidNativeUtil; +import com.codename1.io.Log; +import com.codename1.ui.Display; +import com.codename1.ui.Image; +import com.codename1.util.SuccessCallback; + +class AndroidScreenshotTask implements Runnable { + + private final CodenameOneSurface view; + private final Activity activity; + private final SuccessCallback callback; + + AndroidScreenshotTask(CodenameOneSurface view, Activity activity, SuccessCallback callback) { + this.view = view; + this.activity = activity; + this.callback = callback; + } + + public void run() { + final int w = view.getViewWidth(); + final int h = view.getViewHeight(); + + if (w <= 0 || h <= 0) { + postError(new IllegalStateException("View not laid out yet")); + return; + } + + if (Build.VERSION.SDK_INT >= 26) { + tryPixelCopy(w, h); + return; + } + + // Pre-Oreo: fallback to drawing the view + tryFallbackDraw(w, h); + } + + private void tryPixelCopy(final int w, final int h) { + try { + final Bitmap target = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + final int[] loc = new int[2]; + ((View)view).getLocationInWindow(loc); + + final android.graphics.Rect src = new android.graphics.Rect( + loc[0], loc[1], loc[0] + w, loc[1] + h + ); + + PixelCopy.request( + activity.getWindow(), + src, + target, + new PixelCopy.OnPixelCopyFinishedListener() { + @Override + public void onPixelCopyFinished(int copyResult) { + if (copyResult == PixelCopy.SUCCESS) { + postSuccess(target); + } else { + // Fallback if PixelCopy fails (e.g., transient surface state) + tryFallbackDraw(w, h); + } + } + }, + new Handler(Looper.getMainLooper()) + ); + } catch (Throwable t) { + // Any unexpected issue → fallback + Log.e(t); + tryFallbackDraw(w, h); + } + } + + private void tryFallbackDraw(int w, int h) { + try { + final Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bmp); + // Draw the view hierarchy (includes background + children) + ((View)view).draw(canvas); + postSuccess(bmp); + } catch (Throwable t) { + Log.e(t); + postError(t); + } + } + + private void postSuccess(final Bitmap bmp) { + if (callback == null) return; + final Image img = Image.createImage(bmp); + Display.getInstance().callSerially(new Runnable() { + @Override public void run() { + callback.onSucess(img); + } + }); + } + + private void postError(final Throwable t) { + Log.e(t); + } +} \ No newline at end of file diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 5f5963ecc2..40848d3d8f 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -5193,6 +5193,47 @@ void com_codename1_impl_ios_IOSNative_updatePersonWithRecordID___int_com_codenam #endif } +/*static UIImage* cn1_captureView(UIView *view) { + CGSize size = view.bounds.size; + if (size.width <= 0 || size.height <= 0) return nil; + + // Prefer drawViewHierarchyInRect (renders with effects), fallback to CALayer + UIGraphicsBeginImageContextWithOptions(size, view.opaque, 0.0); // scale=0 → device scale + BOOL ok = NO; + if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { + ok = [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES]; + } + if (!ok) { + [view.layer renderInContext:UIGraphicsGetCurrentContext()]; + } + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +void com_codename1_impl_ios_IOSNative_screenshot__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { + dispatch_async(dispatch_get_main_queue(), ^{ + UIView *view = [CodenameOne_GLViewController instance].view; + UIImage *img = cn1_captureView(view); + if (!img) { + return; + } + + NSData *png = UIImagePNGRepresentation(img); + if (!png) { + return; + } + + // Create Java byte[] + int len = (int)[png length]; + JAVA_OBJECT byteArr = __NEW_ARRAY_JAVA_BYTE(CN1_THREAD_GET_STATE_PASS_ARG len); + + memcpy((JAVA_ARRAY_BYTE*)((JAVA_ARRAY)byteArr)->data, (const jbyte*)[png bytes], len); + + com_codename1_impl_ios_IOSImplementation_onScreenshot___byte_1ARRAY(CN1_THREAD_GET_STATE_PASS_ARG byteArr); + }); +}*/ + JAVA_LONG com_codename1_impl_ios_IOSNative_getPersonWithRecordID___int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT recId) { #ifdef INCLUDE_CONTACTS_USAGE diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 12190a35cb..b23b41f579 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -33,6 +33,7 @@ import com.codename1.location.Location; import com.codename1.ui.Component; import com.codename1.ui.Display; +import com.codename1.ui.EncodedImage; import com.codename1.ui.Font; import com.codename1.ui.Image; import com.codename1.ui.PeerComponent; @@ -300,6 +301,25 @@ public void addCookie(Cookie c) { } } + /*private static SuccessCallback screenshotCallback; + + @Override + public void screenshot(SuccessCallback callback) { + screenshotCallback = callback; + nativeInstance.screenshot(); + } + + static void onScreenshot(final byte[] imageData) { + if(screenshotCallback != null) { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + screenshotCallback.onSucess(EncodedImage.createImage(imageData)); + } + }); + } + }*/ + /** * Used to enable/disable native cookies from native code. * @param cookiesArray diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 010e59d315..7989e1ccc2 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -691,6 +691,7 @@ native void nativeSetTransformMutable( // We go through java in order to use locking concurrency native void appendData(long peer, long data); + //native void screenshot(); native void fillPolygonGlobal(int color, int alpha, int[] xPoints, int[] yPoints, int nPoints);