1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.app;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Resources;
22import android.graphics.Bitmap;
23import android.graphics.BitmapFactory;
24import android.graphics.Canvas;
25import android.graphics.ColorFilter;
26import android.graphics.Paint;
27import android.graphics.PixelFormat;
28import android.graphics.Rect;
29import android.graphics.drawable.BitmapDrawable;
30import android.graphics.drawable.Drawable;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.IBinder;
34import android.os.Looper;
35import android.os.Message;
36import android.os.ParcelFileDescriptor;
37import android.os.RemoteException;
38import android.os.ServiceManager;
39import android.util.DisplayMetrics;
40import android.util.Log;
41import android.view.ViewRoot;
42
43import java.io.FileOutputStream;
44import java.io.IOException;
45import java.io.InputStream;
46
47/**
48 * Provides access to the system wallpaper. With WallpaperManager, you can
49 * get the current wallpaper, get the desired dimensions for the wallpaper, set
50 * the wallpaper, and more. Get an instance of WallpaperManager with
51 * {@link #getInstance(android.content.Context) getInstance()}.
52 */
53public class WallpaperManager {
54    private static String TAG = "WallpaperManager";
55    private static boolean DEBUG = false;
56    private float mWallpaperXStep = -1;
57    private float mWallpaperYStep = -1;
58
59    /**
60     * Launch an activity for the user to pick the current global live
61     * wallpaper.
62     */
63    public static final String ACTION_LIVE_WALLPAPER_CHOOSER
64            = "android.service.wallpaper.LIVE_WALLPAPER_CHOOSER";
65
66    /**
67     * Command for {@link #sendWallpaperCommand}: reported by the wallpaper
68     * host when the user taps on an empty area (not performing an action
69     * in the host).  The x and y arguments are the location of the tap in
70     * screen coordinates.
71     */
72    public static final String COMMAND_TAP = "android.wallpaper.tap";
73
74    /**
75     * Command for {@link #sendWallpaperCommand}: reported by the wallpaper
76     * host when the user drops an object into an area of the host.  The x
77     * and y arguments are the location of the drop.
78     */
79    public static final String COMMAND_DROP = "android.home.drop";
80
81    private final Context mContext;
82
83    /**
84     * Special drawable that draws a wallpaper as fast as possible.  Assumes
85     * no scaling or placement off (0,0) of the wallpaper (this should be done
86     * at the time the bitmap is loaded).
87     */
88    static class FastBitmapDrawable extends Drawable {
89        private final Bitmap mBitmap;
90        private final int mWidth;
91        private final int mHeight;
92        private int mDrawLeft;
93        private int mDrawTop;
94
95        private FastBitmapDrawable(Bitmap bitmap) {
96            mBitmap = bitmap;
97            mWidth = bitmap.getWidth();
98            mHeight = bitmap.getHeight();
99            setBounds(0, 0, mWidth, mHeight);
100        }
101
102        @Override
103        public void draw(Canvas canvas) {
104            canvas.drawBitmap(mBitmap, mDrawLeft, mDrawTop, null);
105        }
106
107        @Override
108        public int getOpacity() {
109            return PixelFormat.OPAQUE;
110        }
111
112        @Override
113        public void setBounds(int left, int top, int right, int bottom) {
114            mDrawLeft = left + (right-left - mWidth) / 2;
115            mDrawTop = top + (bottom-top - mHeight) / 2;
116        }
117
118        @Override
119        public void setBounds(Rect bounds) {
120            // TODO Auto-generated method stub
121            super.setBounds(bounds);
122        }
123
124        @Override
125        public void setAlpha(int alpha) {
126            throw new UnsupportedOperationException(
127                    "Not supported with this drawable");
128        }
129
130        @Override
131        public void setColorFilter(ColorFilter cf) {
132            throw new UnsupportedOperationException(
133                    "Not supported with this drawable");
134        }
135
136        @Override
137        public void setDither(boolean dither) {
138            throw new UnsupportedOperationException(
139                    "Not supported with this drawable");
140        }
141
142        @Override
143        public void setFilterBitmap(boolean filter) {
144            throw new UnsupportedOperationException(
145                    "Not supported with this drawable");
146        }
147
148        @Override
149        public int getIntrinsicWidth() {
150            return mWidth;
151        }
152
153        @Override
154        public int getIntrinsicHeight() {
155            return mHeight;
156        }
157
158        @Override
159        public int getMinimumWidth() {
160            return mWidth;
161        }
162
163        @Override
164        public int getMinimumHeight() {
165            return mHeight;
166        }
167    }
168
169    static class Globals extends IWallpaperManagerCallback.Stub {
170        private IWallpaperManager mService;
171        private Bitmap mWallpaper;
172        private Bitmap mDefaultWallpaper;
173
174        private static final int MSG_CLEAR_WALLPAPER = 1;
175
176        private final Handler mHandler;
177
178        Globals(Looper looper) {
179            IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE);
180            mService = IWallpaperManager.Stub.asInterface(b);
181            mHandler = new Handler(looper) {
182                @Override
183                public void handleMessage(Message msg) {
184                    switch (msg.what) {
185                        case MSG_CLEAR_WALLPAPER:
186                            synchronized (this) {
187                                mWallpaper = null;
188                                mDefaultWallpaper = null;
189                            }
190                            break;
191                    }
192                }
193            };
194        }
195
196        public void onWallpaperChanged() {
197            /* The wallpaper has changed but we shouldn't eagerly load the
198             * wallpaper as that would be inefficient. Reset the cached wallpaper
199             * to null so if the user requests the wallpaper again then we'll
200             * fetch it.
201             */
202            mHandler.sendEmptyMessage(MSG_CLEAR_WALLPAPER);
203        }
204
205        public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) {
206            synchronized (this) {
207                if (mWallpaper != null) {
208                    return mWallpaper;
209                }
210                if (mDefaultWallpaper != null) {
211                    return mDefaultWallpaper;
212                }
213                mWallpaper = null;
214                try {
215                    mWallpaper = getCurrentWallpaperLocked(context);
216                } catch (OutOfMemoryError e) {
217                    Log.w(TAG, "No memory load current wallpaper", e);
218                }
219                if (mWallpaper == null && returnDefault) {
220                    mDefaultWallpaper = getDefaultWallpaperLocked(context);
221                    return mDefaultWallpaper;
222                }
223                return mWallpaper;
224            }
225        }
226
227        private Bitmap getCurrentWallpaperLocked(Context context) {
228            try {
229                Bundle params = new Bundle();
230                ParcelFileDescriptor fd = mService.getWallpaper(this, params);
231                if (fd != null) {
232                    int width = params.getInt("width", 0);
233                    int height = params.getInt("height", 0);
234
235                    if (width <= 0 || height <= 0) {
236                        // Degenerate case: no size requested, just load
237                        // bitmap as-is.
238                        Bitmap bm = BitmapFactory.decodeFileDescriptor(
239                                fd.getFileDescriptor(), null, null);
240                        try {
241                            fd.close();
242                        } catch (IOException e) {
243                        }
244                        if (bm != null) {
245                            bm.setDensity(DisplayMetrics.DENSITY_DEVICE);
246                        }
247                        return bm;
248                    }
249
250                    // Load the bitmap with full color depth, to preserve
251                    // quality for later processing.
252                    BitmapFactory.Options options = new BitmapFactory.Options();
253                    options.inDither = false;
254                    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
255                    Bitmap bm = BitmapFactory.decodeFileDescriptor(
256                            fd.getFileDescriptor(), null, options);
257                    try {
258                        fd.close();
259                    } catch (IOException e) {
260                    }
261
262                    return generateBitmap(context, bm, width, height);
263                }
264            } catch (RemoteException e) {
265            }
266            return null;
267        }
268
269        private Bitmap getDefaultWallpaperLocked(Context context) {
270            try {
271                InputStream is = context.getResources().openRawResource(
272                        com.android.internal.R.drawable.default_wallpaper);
273                if (is != null) {
274                    int width = mService.getWidthHint();
275                    int height = mService.getHeightHint();
276
277                    if (width <= 0 || height <= 0) {
278                        // Degenerate case: no size requested, just load
279                        // bitmap as-is.
280                        Bitmap bm = BitmapFactory.decodeStream(is, null, null);
281                        try {
282                            is.close();
283                        } catch (IOException e) {
284                        }
285                        if (bm != null) {
286                            bm.setDensity(DisplayMetrics.DENSITY_DEVICE);
287                        }
288                        return bm;
289                    }
290
291                    // Load the bitmap with full color depth, to preserve
292                    // quality for later processing.
293                    BitmapFactory.Options options = new BitmapFactory.Options();
294                    options.inDither = false;
295                    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
296                    Bitmap bm = BitmapFactory.decodeStream(is, null, options);
297                    try {
298                        is.close();
299                    } catch (IOException e) {
300                    }
301
302                    try {
303                        return generateBitmap(context, bm, width, height);
304                    } catch (OutOfMemoryError e) {
305                        Log.w(TAG, "Can't generate default bitmap", e);
306                        return bm;
307                    }
308                }
309            } catch (RemoteException e) {
310            }
311            return null;
312        }
313    }
314
315    private static Object mSync = new Object();
316    private static Globals sGlobals;
317
318    static void initGlobals(Looper looper) {
319        synchronized (mSync) {
320            if (sGlobals == null) {
321                sGlobals = new Globals(looper);
322            }
323        }
324    }
325
326    /*package*/ WallpaperManager(Context context, Handler handler) {
327        mContext = context;
328        initGlobals(context.getMainLooper());
329    }
330
331    /**
332     * Retrieve a WallpaperManager associated with the given Context.
333     */
334    public static WallpaperManager getInstance(Context context) {
335        return (WallpaperManager)context.getSystemService(
336                Context.WALLPAPER_SERVICE);
337    }
338
339    /** @hide */
340    public IWallpaperManager getIWallpaperManager() {
341        return sGlobals.mService;
342    }
343
344    /**
345     * Retrieve the current system wallpaper; if
346     * no wallpaper is set, the system default wallpaper is returned.
347     * This is returned as an
348     * abstract Drawable that you can install in a View to display whatever
349     * wallpaper the user has currently set.
350     *
351     * @return Returns a Drawable object that will draw the wallpaper.
352     */
353    public Drawable getDrawable() {
354        Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true);
355        if (bm != null) {
356            Drawable dr = new BitmapDrawable(mContext.getResources(), bm);
357            dr.setDither(false);
358            return dr;
359        }
360        return null;
361    }
362
363    /**
364     * Retrieve the current system wallpaper; if there is no wallpaper set,
365     * a null pointer is returned. This is returned as an
366     * abstract Drawable that you can install in a View to display whatever
367     * wallpaper the user has currently set.
368     *
369     * @return Returns a Drawable object that will draw the wallpaper or a
370     * null pointer if these is none.
371     */
372    public Drawable peekDrawable() {
373        Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false);
374        if (bm != null) {
375            Drawable dr = new BitmapDrawable(mContext.getResources(), bm);
376            dr.setDither(false);
377            return dr;
378        }
379        return null;
380    }
381
382    /**
383     * Like {@link #getDrawable()}, but the returned Drawable has a number
384     * of limitations to reduce its overhead as much as possible. It will
385     * never scale the wallpaper (only centering it if the requested bounds
386     * do match the bitmap bounds, which should not be typical), doesn't
387     * allow setting an alpha, color filter, or other attributes, etc.  The
388     * bounds of the returned drawable will be initialized to the same bounds
389     * as the wallpaper, so normally you will not need to touch it.  The
390     * drawable also assumes that it will be used in a context running in
391     * the same density as the screen (not in density compatibility mode).
392     *
393     * @return Returns a Drawable object that will draw the wallpaper.
394     */
395    public Drawable getFastDrawable() {
396        Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true);
397        if (bm != null) {
398            Drawable dr = new FastBitmapDrawable(bm);
399            return dr;
400        }
401        return null;
402    }
403
404    /**
405     * Like {@link #getFastDrawable()}, but if there is no wallpaper set,
406     * a null pointer is returned.
407     *
408     * @return Returns an optimized Drawable object that will draw the
409     * wallpaper or a null pointer if these is none.
410     */
411    public Drawable peekFastDrawable() {
412        Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false);
413        if (bm != null) {
414            Drawable dr = new FastBitmapDrawable(bm);
415            return dr;
416        }
417        return null;
418    }
419
420    /**
421     * If the current wallpaper is a live wallpaper component, return the
422     * information about that wallpaper.  Otherwise, if it is a static image,
423     * simply return null.
424     */
425    public WallpaperInfo getWallpaperInfo() {
426        try {
427            return sGlobals.mService.getWallpaperInfo();
428        } catch (RemoteException e) {
429            return null;
430        }
431    }
432
433    /**
434     * Change the current system wallpaper to the bitmap in the given resource.
435     * The resource is opened as a raw data stream and copied into the
436     * wallpaper; it must be a valid PNG or JPEG image.  On success, the intent
437     * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
438     *
439     * @param resid The bitmap to save.
440     *
441     * @throws IOException If an error occurs reverting to the default
442     * wallpaper.
443     */
444    public void setResource(int resid) throws IOException {
445        try {
446            Resources resources = mContext.getResources();
447            /* Set the wallpaper to the default values */
448            ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(
449                    "res:" + resources.getResourceName(resid));
450            if (fd != null) {
451                FileOutputStream fos = null;
452                try {
453                    fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
454                    setWallpaper(resources.openRawResource(resid), fos);
455                } finally {
456                    if (fos != null) {
457                        fos.close();
458                    }
459                }
460            }
461        } catch (RemoteException e) {
462        }
463    }
464
465    /**
466     * Change the current system wallpaper to a bitmap.  The given bitmap is
467     * converted to a PNG and stored as the wallpaper.  On success, the intent
468     * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
469     *
470     * @param bitmap The bitmap to save.
471     *
472     * @throws IOException If an error occurs reverting to the default
473     * wallpaper.
474     */
475    public void setBitmap(Bitmap bitmap) throws IOException {
476        try {
477            ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null);
478            if (fd == null) {
479                return;
480            }
481            FileOutputStream fos = null;
482            try {
483                fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
484                bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
485            } finally {
486                if (fos != null) {
487                    fos.close();
488                }
489            }
490        } catch (RemoteException e) {
491        }
492    }
493
494    /**
495     * Change the current system wallpaper to a specific byte stream.  The
496     * give InputStream is copied into persistent storage and will now be
497     * used as the wallpaper.  Currently it must be either a JPEG or PNG
498     * image.  On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
499     * is broadcast.
500     *
501     * @param data A stream containing the raw data to install as a wallpaper.
502     *
503     * @throws IOException If an error occurs reverting to the default
504     * wallpaper.
505     */
506    public void setStream(InputStream data) throws IOException {
507        try {
508            ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null);
509            if (fd == null) {
510                return;
511            }
512            FileOutputStream fos = null;
513            try {
514                fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
515                setWallpaper(data, fos);
516            } finally {
517                if (fos != null) {
518                    fos.close();
519                }
520            }
521        } catch (RemoteException e) {
522        }
523    }
524
525    private void setWallpaper(InputStream data, FileOutputStream fos)
526            throws IOException {
527        byte[] buffer = new byte[32768];
528        int amt;
529        while ((amt=data.read(buffer)) > 0) {
530            fos.write(buffer, 0, amt);
531        }
532    }
533
534    /**
535     * Returns the desired minimum width for the wallpaper. Callers of
536     * {@link #setBitmap(android.graphics.Bitmap)} or
537     * {@link #setStream(java.io.InputStream)} should check this value
538     * beforehand to make sure the supplied wallpaper respects the desired
539     * minimum width.
540     *
541     * If the returned value is <= 0, the caller should use the width of
542     * the default display instead.
543     *
544     * @return The desired minimum width for the wallpaper. This value should
545     * be honored by applications that set the wallpaper but it is not
546     * mandatory.
547     */
548    public int getDesiredMinimumWidth() {
549        try {
550            return sGlobals.mService.getWidthHint();
551        } catch (RemoteException e) {
552            // Shouldn't happen!
553            return 0;
554        }
555    }
556
557    /**
558     * Returns the desired minimum height for the wallpaper. Callers of
559     * {@link #setBitmap(android.graphics.Bitmap)} or
560     * {@link #setStream(java.io.InputStream)} should check this value
561     * beforehand to make sure the supplied wallpaper respects the desired
562     * minimum height.
563     *
564     * If the returned value is <= 0, the caller should use the height of
565     * the default display instead.
566     *
567     * @return The desired minimum height for the wallpaper. This value should
568     * be honored by applications that set the wallpaper but it is not
569     * mandatory.
570     */
571    public int getDesiredMinimumHeight() {
572        try {
573            return sGlobals.mService.getHeightHint();
574        } catch (RemoteException e) {
575            // Shouldn't happen!
576            return 0;
577        }
578    }
579
580    /**
581     * For use only by the current home application, to specify the size of
582     * wallpaper it would like to use.  This allows such applications to have
583     * a virtual wallpaper that is larger than the physical screen, matching
584     * the size of their workspace.
585     * @param minimumWidth Desired minimum width
586     * @param minimumHeight Desired minimum height
587     */
588    public void suggestDesiredDimensions(int minimumWidth, int minimumHeight) {
589        try {
590            sGlobals.mService.setDimensionHints(minimumWidth, minimumHeight);
591        } catch (RemoteException e) {
592        }
593    }
594
595    /**
596     * Set the position of the current wallpaper within any larger space, when
597     * that wallpaper is visible behind the given window.  The X and Y offsets
598     * are floating point numbers ranging from 0 to 1, representing where the
599     * wallpaper should be positioned within the screen space.  These only
600     * make sense when the wallpaper is larger than the screen.
601     *
602     * @param windowToken The window who these offsets should be associated
603     * with, as returned by {@link android.view.View#getWindowToken()
604     * View.getWindowToken()}.
605     * @param xOffset The offset along the X dimension, from 0 to 1.
606     * @param yOffset The offset along the Y dimension, from 0 to 1.
607     */
608    public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) {
609        try {
610            //Log.v(TAG, "Sending new wallpaper offsets from app...");
611            ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition(
612                    windowToken, xOffset, yOffset, mWallpaperXStep, mWallpaperYStep);
613            //Log.v(TAG, "...app returning after sending offsets!");
614        } catch (RemoteException e) {
615            // Ignore.
616        }
617    }
618
619    /**
620     * For applications that use multiple virtual screens showing a wallpaper,
621     * specify the step size between virtual screens. For example, if the
622     * launcher has 3 virtual screens, it would specify an xStep of 0.5,
623     * since the X offset for those screens are 0.0, 0.5 and 1.0
624     * @param xStep The X offset delta from one screen to the next one
625     * @param yStep The Y offset delta from one screen to the next one
626     */
627    public void setWallpaperOffsetSteps(float xStep, float yStep) {
628        mWallpaperXStep = xStep;
629        mWallpaperYStep = yStep;
630    }
631
632    /**
633     * Send an arbitrary command to the current active wallpaper.
634     *
635     * @param windowToken The window who these offsets should be associated
636     * with, as returned by {@link android.view.View#getWindowToken()
637     * View.getWindowToken()}.
638     * @param action Name of the command to perform.  This must be a scoped
639     * name to avoid collisions, such as "com.mycompany.wallpaper.DOIT".
640     * @param x Arbitrary integer argument based on command.
641     * @param y Arbitrary integer argument based on command.
642     * @param z Arbitrary integer argument based on command.
643     * @param extras Optional additional information for the command, or null.
644     */
645    public void sendWallpaperCommand(IBinder windowToken, String action,
646            int x, int y, int z, Bundle extras) {
647        try {
648            //Log.v(TAG, "Sending new wallpaper offsets from app...");
649            ViewRoot.getWindowSession(mContext.getMainLooper()).sendWallpaperCommand(
650                    windowToken, action, x, y, z, extras, false);
651            //Log.v(TAG, "...app returning after sending offsets!");
652        } catch (RemoteException e) {
653            // Ignore.
654        }
655    }
656
657    /**
658     * Clear the offsets previously associated with this window through
659     * {@link #setWallpaperOffsets(IBinder, float, float)}.  This reverts
660     * the window to its default state, where it does not cause the wallpaper
661     * to scroll from whatever its last offsets were.
662     *
663     * @param windowToken The window who these offsets should be associated
664     * with, as returned by {@link android.view.View#getWindowToken()
665     * View.getWindowToken()}.
666     */
667    public void clearWallpaperOffsets(IBinder windowToken) {
668        try {
669            ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition(
670                    windowToken, -1, -1, -1, -1);
671        } catch (RemoteException e) {
672            // Ignore.
673        }
674    }
675
676    /**
677     * Remove any currently set wallpaper, reverting to the system's default
678     * wallpaper. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
679     * is broadcast.
680     *
681     * @throws IOException If an error occurs reverting to the default
682     * wallpaper.
683     */
684    public void clear() throws IOException {
685        setResource(com.android.internal.R.drawable.default_wallpaper);
686    }
687
688    static Bitmap generateBitmap(Context context, Bitmap bm, int width, int height) {
689        if (bm == null) {
690            return bm;
691        }
692        bm.setDensity(DisplayMetrics.DENSITY_DEVICE);
693
694        // This is the final bitmap we want to return.
695        // XXX We should get the pixel depth from the system (to match the
696        // physical display depth), when there is a way.
697        Bitmap newbm = Bitmap.createBitmap(width, height,
698                Bitmap.Config.RGB_565);
699        newbm.setDensity(DisplayMetrics.DENSITY_DEVICE);
700        Canvas c = new Canvas(newbm);
701        c.setDensity(DisplayMetrics.DENSITY_DEVICE);
702        Rect targetRect = new Rect();
703        targetRect.left = targetRect.top = 0;
704        targetRect.right = bm.getWidth();
705        targetRect.bottom = bm.getHeight();
706
707        int deltaw = width - targetRect.right;
708        int deltah = height - targetRect.bottom;
709
710        if (deltaw > 0 || deltah > 0) {
711            // We need to scale up so it covers the entire
712            // area.
713            float scale = 1.0f;
714            if (deltaw > deltah) {
715                scale = width / (float)targetRect.right;
716            } else {
717                scale = height / (float)targetRect.bottom;
718            }
719            targetRect.right = (int)(targetRect.right*scale);
720            targetRect.bottom = (int)(targetRect.bottom*scale);
721            deltaw = width - targetRect.right;
722            deltah = height - targetRect.bottom;
723        }
724
725        targetRect.offset(deltaw/2, deltah/2);
726        Paint paint = new Paint();
727        paint.setFilterBitmap(true);
728        paint.setDither(true);
729        c.drawBitmap(bm, null, targetRect, paint);
730
731        bm.recycle();
732        return newbm;
733    }
734}
735