1/*
2 * Copyright (C) 2011 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.videoeditor;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Paint;
26import android.graphics.Paint.Style;
27import android.graphics.Typeface;
28import android.media.videoeditor.VideoEditor;
29import android.os.AsyncTask;
30import android.text.format.DateUtils;
31import android.text.TextPaint;
32import android.text.TextUtils;
33import android.util.Log;
34import android.util.LruCache;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.ViewGroup;
38import android.widget.BaseAdapter;
39import android.widget.ImageView;
40import android.widget.TextView;
41
42import com.android.videoeditor.service.VideoEditorProject;
43import com.android.videoeditor.util.ImageUtils;
44
45import java.io.File;
46import java.io.IOException;
47import java.util.List;
48
49
50public class ProjectPickerAdapter extends BaseAdapter {
51    private Context mContext;
52    private Resources mResources;
53    private LayoutInflater mInflater;
54    private List<VideoEditorProject> mProjects;
55    private int mItemWidth;
56    private int mItemHeight;
57    private int mOverlayHeight;
58    private int mOverlayVerticalInset;
59    private int mOverlayHorizontalInset;
60    private LruCache<String, Bitmap> mPreviewBitmapCache;
61
62    public ProjectPickerAdapter(Context context, LayoutInflater inflater,
63            List<VideoEditorProject> projects) {
64        mContext = context;
65        mResources = context.getResources();
66        mInflater = inflater;
67        mProjects = projects;
68        mItemWidth = (int) mResources.getDimension(R.dimen.project_picker_item_width);
69        mItemHeight = (int) mResources.getDimension(R.dimen.project_picker_item_height);
70        mOverlayHeight = (int) mResources.getDimension(
71                R.dimen.project_picker_item_overlay_height);
72        mOverlayVerticalInset = (int) mResources.getDimension(
73                R.dimen.project_picker_item_overlay_vertical_inset);
74        mOverlayHorizontalInset = (int) mResources.getDimension(
75                R.dimen.project_picker_item_overlay_horizontal_inset);
76        // Limit the cache size to 15 thumbnails.
77        mPreviewBitmapCache = new LruCache<String, Bitmap>(15);
78    }
79
80    /**
81     * Clears project list and update display.
82     */
83    public void clear() {
84        mPreviewBitmapCache.evictAll();
85        mProjects.clear();
86        notifyDataSetChanged();
87    }
88
89    /**
90     * Removes the project with specified {@code projectPath} from the project list and updates the
91     * display.
92     *
93     * @param projectPath The project path of the to-be-removed project
94     * @return {@code true} if the project is successfully removed,
95     *      {@code false} if no removal happened
96     */
97    public boolean remove(String projectPath) {
98        for (VideoEditorProject project : mProjects) {
99            if (project.getPath().equals(projectPath)) {
100                if (mProjects.remove(project)) {
101                    notifyDataSetChanged();
102                    return true;
103                } else {
104                    return false;
105                }
106            }
107        }
108        return false;
109    }
110
111    @Override
112    public int getCount() {
113        // Add one to represent an additional dummy project for "create new project" option.
114        return mProjects.size() + 1;
115    }
116
117    @Override
118    public Object getItem(int position) {
119        if (position == mProjects.size()) {
120            return null;
121        }
122        return mProjects.get(position);
123    }
124
125    @Override
126    public long getItemId(int position) {
127        return position;
128    }
129
130    @Override
131    public View getView(int position, View convertView, ViewGroup parent) {
132        // Inflate a new view with project thumbnail and information.
133        // We never reuse convertView because we load thumbnails asynchronously
134        // and hook an async task with the new view. If the new view is reused
135        // as a convertView, the async task might put a wrong thumbnail on it.
136        View v = mInflater.inflate(R.layout.project_picker_item, null);
137        ImageView iv = (ImageView) v.findViewById(R.id.thumbnail);
138        Bitmap thumbnail;
139        String title;
140        String duration;
141        if (position == mProjects.size()) {
142            title = mContext.getString(R.string.projects_new_project);
143            duration = "";
144            thumbnail = renderNewProjectThumbnail();
145        } else {
146            VideoEditorProject project = mProjects.get(position);
147            title = project.getName();
148            if (title == null) {
149                title = "";
150            }
151            duration = millisecondsToTimeString(project.getProjectDuration());
152            thumbnail = getThumbnail(project.getPath(), iv, title, duration);
153        }
154
155        if (thumbnail != null) {
156            drawBottomOverlay(thumbnail, title, duration);
157            iv.setImageBitmap(thumbnail);
158        }
159
160        return v;
161    }
162
163    /**
164     * Draws transparent black bottom overlay with movie title and duration on the bitmap.
165     */
166    public void drawBottomOverlay(Bitmap bitmap, String title, String duration) {
167        // Draw overlay at the bottom of the canvas.
168        final Canvas canvas = new Canvas(bitmap);
169        final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
170        paint.setColor(Color.BLACK);
171        paint.setAlpha(128);
172        final int left = 0, top = bitmap.getHeight() - mOverlayHeight,
173                right = bitmap.getWidth(), bottom = bitmap.getHeight();
174        canvas.drawRect(left, top, right, bottom, paint);
175
176        paint.setColor(Color.WHITE);
177        paint.setTextSize((int) mResources.getDimension(R.dimen.project_picker_item_font_size));
178
179        // Draw movie title at the left of the overlay. Trim title if it is going to overlap with
180        // duration text.
181        final int availableTitleWidth = bitmap.getWidth() - (int) paint.measureText(duration);
182        title = TextUtils.ellipsize(title, new TextPaint(paint), availableTitleWidth,
183                TextUtils.TruncateAt.END).toString();
184        canvas.drawText(title, mOverlayHorizontalInset,
185                bitmap.getHeight() - mOverlayHeight + mOverlayVerticalInset,
186                paint);
187
188        // Draw movie duration at the right of the overlay.
189        canvas.drawText(duration,
190                bitmap.getWidth() - paint.measureText(duration) - mOverlayHorizontalInset,
191                bitmap.getHeight() - mOverlayHeight + mOverlayVerticalInset,
192                paint);
193    }
194
195    private Bitmap getThumbnail(String projectPath, ImageView imageView, String title,
196            String duration) {
197        Bitmap previewBitmap = mPreviewBitmapCache.get(projectPath);
198        if (previewBitmap == null) {
199            // Cache miss: asynchronously load bitmap to avoid scroll stuttering
200            // in the project picker.
201            new LoadPreviewBitmapTask(this, projectPath, imageView, mItemWidth, mItemHeight,
202                    title, duration, mPreviewBitmapCache).execute();
203        } else {
204            return previewBitmap;
205        }
206
207        return null;
208    }
209
210    private Bitmap renderNewProjectThumbnail() {
211        final Bitmap bitmap = Bitmap.createBitmap(mItemWidth, mItemHeight,
212                Bitmap.Config.ARGB_8888);
213        final Canvas canvas = new Canvas(bitmap);
214        final Paint paint = new Paint();
215        canvas.drawRect(0, 0, mItemWidth, mItemHeight, paint);
216
217        paint.setTextSize(18.0f);
218        paint.setAlpha(255);
219        final Bitmap newProjectIcon = BitmapFactory.decodeResource(mResources,
220                R.drawable.add_video_project_big);
221        final int x = (mItemWidth - newProjectIcon.getWidth()) / 2;
222        final int y = (mItemHeight - newProjectIcon.getHeight()) / 2;
223        canvas.drawBitmap(newProjectIcon, x, y, paint);
224        newProjectIcon.recycle();
225
226        return bitmap;
227    }
228
229    /**
230     * Converts milliseconds into the string time format HH:mm:ss.
231     */
232    private String millisecondsToTimeString(long milliseconds) {
233        return DateUtils.formatElapsedTime(milliseconds / 1000);
234    }
235}
236
237/**
238 * Worker that loads preview bitmap for a project,
239 */
240class LoadPreviewBitmapTask extends AsyncTask<Void, Void, Bitmap> {
241    // Handle to the adapter that initiates this async task.
242    private ProjectPickerAdapter mContextAdapter;
243    private String mProjectPath;
244    // Handle to the image view we should update when the preview bitmap is loaded.
245    private ImageView mImageView;
246    private int mWidth;
247    private int mHeight;
248    private String mTitle;
249    private String mDuration;
250    private LruCache<String, Bitmap> mPreviewBitmapCache;
251
252    public LoadPreviewBitmapTask(ProjectPickerAdapter contextAdapter, String projectPath,
253            ImageView imageView, int width, int height, String title, String duration,
254            LruCache<String, Bitmap> previewBitmapCache) {
255        mContextAdapter = contextAdapter;
256        mProjectPath = projectPath;
257        mImageView = imageView;
258        mWidth = width;
259        mHeight = height;
260        mTitle = title;
261        mDuration = duration;
262        mPreviewBitmapCache = previewBitmapCache;
263    }
264
265    @Override
266    protected Bitmap doInBackground(Void... param) {
267        final File thumbnail = new File(mProjectPath, VideoEditor.THUMBNAIL_FILENAME);
268        // Return early if thumbnail does not exist.
269        if (!thumbnail.exists()) {
270            return null;
271        }
272
273        try {
274            final Bitmap previewBitmap = ImageUtils.scaleImage(
275                    thumbnail.getAbsolutePath(),
276                    mWidth,
277                    mHeight,
278                    ImageUtils.MATCH_LARGER_DIMENSION);
279            if (previewBitmap != null) {
280                final Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight,
281                        Bitmap.Config.ARGB_8888);
282                final Canvas canvas = new Canvas(bitmap);
283                final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
284
285                // Draw bitmap at the center of the canvas.
286                canvas.drawBitmap(previewBitmap,
287                        (mWidth - previewBitmap.getWidth()) / 2,
288                        (mHeight - previewBitmap.getHeight()) / 2,
289                        paint);
290                return bitmap;
291            }
292        } catch (IOException e) {
293            e.printStackTrace();
294        }
295
296        return null;
297    }
298
299    @Override
300    protected void onPostExecute(Bitmap result) {
301        if (result == null) {
302            // If we don't have thumbnail, default to a black canvas.
303            result = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
304            result.eraseColor(Color.BLACK);
305        } else {
306            mPreviewBitmapCache.put(mProjectPath, result);
307        }
308
309        // Update the image view.
310        mContextAdapter.drawBottomOverlay(result, mTitle, mDuration);
311        mImageView.setImageBitmap(result);
312    }
313}
314