1/*
2 * Copyright (C) 2013 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 com.android.wallpaperpicker;
18
19import android.annotation.TargetApi;
20import android.app.ActionBar;
21import android.app.Activity;
22import android.app.WallpaperManager;
23import android.content.DialogInterface;
24import android.content.Intent;
25import android.content.pm.ActivityInfo;
26import android.content.res.Resources;
27import android.graphics.Bitmap;
28import android.graphics.Matrix;
29import android.graphics.Point;
30import android.graphics.RectF;
31import android.graphics.drawable.Drawable;
32import android.net.Uri;
33import android.os.Build;
34import android.os.Bundle;
35import android.os.Handler;
36import android.os.HandlerThread;
37import android.os.Message;
38import android.util.Log;
39import android.view.Display;
40import android.view.View;
41import android.widget.Toast;
42
43import com.android.wallpaperpicker.common.CropAndSetWallpaperTask;
44import com.android.gallery3d.common.Utils;
45import com.android.photos.BitmapRegionTileSource;
46import com.android.photos.BitmapRegionTileSource.BitmapSource;
47import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider;
48import com.android.photos.views.TiledImageRenderer.TileSource;
49import com.android.wallpaperpicker.common.DialogUtils;
50import com.android.wallpaperpicker.common.InputStreamProvider;
51
52import java.util.Collections;
53import java.util.Set;
54import java.util.WeakHashMap;
55
56public class WallpaperCropActivity extends Activity implements Handler.Callback {
57    private static final String LOGTAG = "WallpaperCropActivity";
58
59    private static final int MSG_LOAD_IMAGE = 1;
60
61    protected CropView mCropView;
62    protected View mProgressView;
63    protected View mSetWallpaperButton;
64
65    private HandlerThread mLoaderThread;
66    private Handler mLoaderHandler;
67    private LoadRequest mCurrentLoadRequest;
68    private byte[] mTempStorageForDecoding = new byte[16 * 1024];
69    // A weak-set of reusable bitmaps
70    private Set<Bitmap> mReusableBitmaps =
71            Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
72
73    private final DialogInterface.OnCancelListener mOnDialogCancelListener =
74            new DialogInterface.OnCancelListener() {
75                @Override
76                public void onCancel(DialogInterface dialog) {
77                    showActionBarAndTiles();
78                }
79            };
80
81    @Override
82    public void onCreate(Bundle savedInstanceState) {
83        super.onCreate(savedInstanceState);
84
85        mLoaderThread = new HandlerThread("wallpaper_loader");
86        mLoaderThread.start();
87        mLoaderHandler = new Handler(mLoaderThread.getLooper(), this);
88
89        init();
90        if (!enableRotation()) {
91            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
92        }
93    }
94
95    protected void init() {
96        setContentView(R.layout.wallpaper_cropper);
97
98        mCropView = (CropView) findViewById(R.id.cropView);
99        mProgressView = findViewById(R.id.loading);
100
101        Intent cropIntent = getIntent();
102        final Uri imageUri = cropIntent.getData();
103
104        if (imageUri == null) {
105            Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity");
106            finish();
107            return;
108        }
109
110        // Action bar
111        // Show the custom action bar view
112        final ActionBar actionBar = getActionBar();
113        actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
114        actionBar.getCustomView().setOnClickListener(
115                new View.OnClickListener() {
116                    @Override
117                    public void onClick(View v) {
118                        actionBar.hide();
119                        // Never fade on finish because we return to the app that started us (e.g.
120                        // Photos), not the home screen.
121                        cropImageAndSetWallpaper(imageUri, null, false /* shouldFadeOutOnFinish */);
122                    }
123                });
124        mSetWallpaperButton = findViewById(R.id.set_wallpaper_button);
125
126        // Load image in background
127        final BitmapRegionTileSource.InputStreamSource bitmapSource =
128                new BitmapRegionTileSource.InputStreamSource(this, imageUri);
129        mSetWallpaperButton.setEnabled(false);
130        Runnable onLoad = new Runnable() {
131            public void run() {
132                if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
133                    Toast.makeText(WallpaperCropActivity.this, R.string.wallpaper_load_fail,
134                            Toast.LENGTH_LONG).show();
135                    finish();
136                } else {
137                    mSetWallpaperButton.setEnabled(true);
138                }
139            }
140        };
141        setCropViewTileSource(bitmapSource, true, false, null, onLoad);
142    }
143
144    @Override
145    public void onDestroy() {
146        if (mCropView != null) {
147            mCropView.destroy();
148        }
149        if (mLoaderThread != null) {
150            mLoaderThread.quit();
151        }
152        super.onDestroy();
153    }
154
155    /**
156     * This is called on {@link #mLoaderThread}
157     */
158    @TargetApi(Build.VERSION_CODES.KITKAT)
159    @Override
160    public boolean handleMessage(Message msg) {
161        if (msg.what == MSG_LOAD_IMAGE) {
162            final LoadRequest req = (LoadRequest) msg.obj;
163            final boolean loadSuccess;
164
165            if (req.src == null) {
166                Drawable defaultWallpaper = WallpaperManager.getInstance(this)
167                        .getBuiltInDrawable(mCropView.getWidth(), mCropView.getHeight(),
168                                false, 0.5f, 0.5f);
169
170                if (defaultWallpaper == null) {
171                    loadSuccess = false;
172                    Log.w(LOGTAG, "Null default wallpaper encountered.");
173                } else {
174                    loadSuccess = true;
175                    req.result = new DrawableTileSource(this,
176                            defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE);
177                }
178            } else {
179                try {
180                    req.src.loadInBackground(new InBitmapProvider() {
181
182                        @Override
183                        public Bitmap forPixelCount(int count) {
184                            Bitmap bitmapToReuse = null;
185                            // Find the smallest bitmap that satisfies the pixel count limit
186                            synchronized (mReusableBitmaps) {
187                                int currentBitmapSize = Integer.MAX_VALUE;
188                                for (Bitmap b : mReusableBitmaps) {
189                                    int bitmapSize = b.getWidth() * b.getHeight();
190                                    if ((bitmapSize >= count) && (bitmapSize < currentBitmapSize)) {
191                                        bitmapToReuse = b;
192                                        currentBitmapSize = bitmapSize;
193                                    }
194                                }
195
196                                if (bitmapToReuse != null) {
197                                    mReusableBitmaps.remove(bitmapToReuse);
198                                }
199                            }
200                            return bitmapToReuse;
201                        }
202                    });
203                } catch (SecurityException securityException) {
204                    if (isActivityDestroyed()) {
205                        // Temporarily granted permissions are revoked when the activity
206                        // finishes, potentially resulting in a SecurityException here.
207                        // Even though {@link #isDestroyed} might also return true in different
208                        // situations where the configuration changes, we are fine with
209                        // catching these cases here as well.
210                        return true;
211                    } else {
212                        // otherwise it had a different cause and we throw it further
213                        throw securityException;
214                    }
215                }
216
217                req.result = new BitmapRegionTileSource(WallpaperCropActivity.this, req.src,
218                        mTempStorageForDecoding);
219                loadSuccess = req.src.getLoadingState() == BitmapSource.State.LOADED;
220            }
221
222            runOnUiThread(new Runnable() {
223
224                @Override
225                public void run() {
226                    if (req == mCurrentLoadRequest) {
227                        onLoadRequestComplete(req, loadSuccess);
228                    } else {
229                        addReusableBitmap(req.result);
230                    }
231                }
232            });
233            return true;
234        }
235        return false;
236    }
237
238    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
239    public boolean isActivityDestroyed() {
240        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed();
241    }
242
243    private void addReusableBitmap(TileSource src) {
244        synchronized (mReusableBitmaps) {
245            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
246                && src instanceof BitmapRegionTileSource) {
247                Bitmap preview = ((BitmapRegionTileSource) src).getBitmap();
248                if (preview != null && preview.isMutable()) {
249                    mReusableBitmaps.add(preview);
250                }
251            }
252        }
253    }
254
255    public DialogInterface.OnCancelListener getOnDialogCancelListener() {
256        return mOnDialogCancelListener;
257    }
258
259    private void showActionBarAndTiles() {
260        getActionBar().show();
261        View wallpaperStrip = findViewById(R.id.wallpaper_strip);
262        if (wallpaperStrip != null) {
263            wallpaperStrip.setVisibility(View.VISIBLE);
264        }
265    }
266
267    protected void onLoadRequestComplete(LoadRequest req, boolean success) {
268        mCurrentLoadRequest = null;
269        if (success) {
270            TileSource oldSrc = mCropView.getTileSource();
271            mCropView.setTileSource(req.result, null);
272            mCropView.setTouchEnabled(req.touchEnabled);
273            if (req.moveToLeft) {
274                mCropView.moveToLeft();
275            }
276            if (req.scaleAndOffsetProvider != null) {
277                TileSource src = req.result;
278                Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize(
279                        getResources(), getWindowManager());
280                RectF crop = Utils.getMaxCropRect(src.getImageWidth(), src.getImageHeight(),
281                        wallpaperSize.x, wallpaperSize.y, false /* leftAligned */);
282                mCropView.setScale(req.scaleAndOffsetProvider.getScale(wallpaperSize, crop));
283                mCropView.setParallaxOffset(req.scaleAndOffsetProvider.getParallaxOffset(), crop);
284            }
285
286            // Free last image
287            if (oldSrc != null) {
288                // Call yield instead of recycle, as we only want to free GL resource.
289                // We can still reuse the bitmap for decoding any other image.
290                oldSrc.getPreview().yield();
291            }
292            addReusableBitmap(oldSrc);
293        }
294        if (req.postExecute != null) {
295            req.postExecute.run();
296        }
297        mProgressView.setVisibility(View.GONE);
298    }
299
300    @TargetApi(Build.VERSION_CODES.KITKAT)
301    public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled,
302            boolean moveToLeft, CropViewScaleAndOffsetProvider scaleAndOffsetProvider,
303            Runnable postExecute) {
304        final LoadRequest req = new LoadRequest();
305        req.moveToLeft = moveToLeft;
306        req.src = bitmapSource;
307        req.touchEnabled = touchEnabled;
308        req.postExecute = postExecute;
309        req.scaleAndOffsetProvider = scaleAndOffsetProvider;
310        mCurrentLoadRequest = req;
311
312        // Remove any pending requests
313        mLoaderHandler.removeMessages(MSG_LOAD_IMAGE);
314        Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget();
315
316        // We don't want to show the spinner every time we load an image, because that would be
317        // annoying; instead, only start showing the spinner if loading the image has taken
318        // longer than 1 sec (ie 1000 ms)
319        mProgressView.postDelayed(new Runnable() {
320            public void run() {
321                if (mCurrentLoadRequest == req) {
322                    mProgressView.setVisibility(View.VISIBLE);
323                }
324            }
325        }, 1000);
326    }
327
328
329    public boolean enableRotation() {
330        return true;
331    }
332
333    public void cropImageAndSetWallpaper(Resources res, int resId, boolean shouldFadeOutOnFinish) {
334        // crop this image and scale it down to the default wallpaper size for
335        // this device
336        InputStreamProvider streamProvider = InputStreamProvider.fromResource(res, resId);
337        Point inSize = mCropView.getSourceDimensions();
338        Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
339                getWindowManager());
340        RectF crop = Utils.getMaxCropRect(
341                inSize.x, inSize.y, outSize.x, outSize.y, false);
342        // Passing 0, 0 will cause launcher to revert to using the
343        // default wallpaper size
344        CropAndFinishHandler onEndCrop = new CropAndFinishHandler(new Point(0, 0),
345                shouldFadeOutOnFinish);
346        CropAndSetWallpaperTask cropTask = new CropAndSetWallpaperTask(
347                streamProvider, this, crop, streamProvider.getRotationFromExif(this),
348                outSize.x, outSize.y, onEndCrop);
349        DialogUtils.executeCropTaskAfterPrompt(this, cropTask, getOnDialogCancelListener());
350    }
351
352    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
353    public void cropImageAndSetWallpaper(Uri uri,
354            CropAndSetWallpaperTask.OnBitmapCroppedHandler onBitmapCroppedHandler,
355            boolean shouldFadeOutOnFinish) {
356        // Get the crop
357        boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
358
359        Display d = getWindowManager().getDefaultDisplay();
360
361        Point displaySize = new Point();
362        d.getSize(displaySize);
363        boolean isPortrait = displaySize.x < displaySize.y;
364
365        Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
366                getWindowManager());
367        // Get the crop
368        RectF cropRect = mCropView.getCrop();
369
370        Point inSize = mCropView.getSourceDimensions();
371
372        int cropRotation = mCropView.getImageRotation();
373        float cropScale = mCropView.getWidth() / (float) cropRect.width();
374
375        Matrix rotateMatrix = new Matrix();
376        rotateMatrix.setRotate(cropRotation);
377        float[] rotatedInSize = new float[] { inSize.x, inSize.y };
378        rotateMatrix.mapPoints(rotatedInSize);
379        rotatedInSize[0] = Math.abs(rotatedInSize[0]);
380        rotatedInSize[1] = Math.abs(rotatedInSize[1]);
381
382        // due to rounding errors in the cropview renderer the edges can be slightly offset
383        // therefore we ensure that the boundaries are sanely defined
384        cropRect.left = Math.max(0, cropRect.left);
385        cropRect.right = Math.min(rotatedInSize[0], cropRect.right);
386        cropRect.top = Math.max(0, cropRect.top);
387        cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom);
388
389        // ADJUST CROP WIDTH
390        // Extend the crop all the way to the right, for parallax
391        // (or all the way to the left, in RTL)
392        float extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
393        // Cap the amount of extra width
394        float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
395        extraSpace = Math.min(extraSpace, maxExtraSpace);
396
397        if (ltr) {
398            cropRect.right += extraSpace;
399        } else {
400            cropRect.left -= extraSpace;
401        }
402
403        // ADJUST CROP HEIGHT
404        if (isPortrait) {
405            cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
406        } else { // LANDSCAPE
407            float extraPortraitHeight =
408                    defaultWallpaperSize.y / cropScale - cropRect.height();
409            float expandHeight =
410                    Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top),
411                            extraPortraitHeight / 2);
412            cropRect.top -= expandHeight;
413            cropRect.bottom += expandHeight;
414        }
415
416        final int outWidth = (int) Math.round(cropRect.width() * cropScale);
417        final int outHeight = (int) Math.round(cropRect.height() * cropScale);
418        CropAndFinishHandler onEndCrop = new CropAndFinishHandler(new Point(outWidth, outHeight),
419                shouldFadeOutOnFinish);
420
421        CropAndSetWallpaperTask cropTask = new CropAndSetWallpaperTask(
422                InputStreamProvider.fromUri(this, uri), this,
423                cropRect, cropRotation, outWidth, outHeight, onEndCrop) {
424            @Override
425            protected void onPreExecute() {
426                // Give some feedback so user knows something is happening.
427                mProgressView.setVisibility(View.VISIBLE);
428            }
429        };
430        if (onBitmapCroppedHandler != null) {
431            cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
432        }
433        DialogUtils.executeCropTaskAfterPrompt(this, cropTask, getOnDialogCancelListener());
434    }
435
436    public void setBoundsAndFinish(Point bounds, boolean overrideTransition) {
437        WallpaperUtils.saveWallpaperDimensions(bounds.x, bounds.y, this);
438        setResult(Activity.RESULT_OK);
439        finish();
440        if (overrideTransition) {
441            overridePendingTransition(0, R.anim.fade_out);
442        }
443    }
444
445    public class CropAndFinishHandler implements CropAndSetWallpaperTask.OnEndCropHandler {
446        private final Point mBounds;
447        private boolean mShouldFadeOutOnFinish;
448
449        /**
450         * @param shouldFadeOutOnFinish Whether the wallpaper picker should override the default
451         * exit animation to fade out instead. This should only be set to true if the wallpaper
452         * preview will exactly match the actual wallpaper on the page we are returning to.
453         */
454        public CropAndFinishHandler(Point bounds, boolean shouldFadeOutOnFinish) {
455            mBounds = bounds;
456            mShouldFadeOutOnFinish = shouldFadeOutOnFinish;
457        }
458
459        @Override
460        public void run(boolean cropSucceeded) {
461            setBoundsAndFinish(mBounds, cropSucceeded && mShouldFadeOutOnFinish);
462        }
463    }
464
465    static class LoadRequest {
466        BitmapSource src;
467        boolean touchEnabled;
468        boolean moveToLeft;
469        Runnable postExecute;
470        CropViewScaleAndOffsetProvider scaleAndOffsetProvider;
471
472        TileSource result;
473    }
474
475    public interface CropViewScaleAndOffsetProvider {
476        float getScale(Point wallpaperSize, RectF crop);
477        float getParallaxOffset();
478    }
479}
480