From d85f53c69dead1f1f6c0290b8104422143bc5166 Mon Sep 17 00:00:00 2001 From: Chris Wren Date: Thu, 6 Sep 2012 16:46:57 -0400 Subject: Add ability to select the albums to display. Change-Id: I80ff33c4c880c445b79735d6483bc9337a89e392 --- AndroidManifest.xml | 25 +- res/layout/album.xml | 19 + res/values/config.xml | 4 + res/values/ids.xml | 1 + res/values/strings.xml | 4 + .../dreams/phototable/AlbumDataAdapter.java | 117 ++++++ .../android/dreams/phototable/AlbumSettings.java | 44 ++ .../android/dreams/phototable/FlipperDream.java | 2 +- .../dreams/phototable/FlipperDreamSettings.java | 52 +++ src/com/android/dreams/phototable/LocalSource.java | 85 +++- .../android/dreams/phototable/PhotoCarousel.java | 16 +- src/com/android/dreams/phototable/PhotoSource.java | 34 +- .../dreams/phototable/PhotoSourcePlexor.java | 45 ++- src/com/android/dreams/phototable/PhotoTable.java | 428 +++++++++++++++++++- .../android/dreams/phototable/PhotoTableDream.java | 40 ++ .../dreams/phototable/PhotoTableDreamSettings.java | 52 +++ .../dreams/phototable/PhotoTouchListener.java | 4 +- .../android/dreams/phototable/PicasaSource.java | 180 ++++++++- src/com/android/dreams/phototable/StockSource.java | 25 +- src/com/android/dreams/phototable/Table.java | 448 --------------------- 20 files changed, 1097 insertions(+), 528 deletions(-) create mode 100644 res/layout/album.xml create mode 100644 src/com/android/dreams/phototable/AlbumDataAdapter.java create mode 100644 src/com/android/dreams/phototable/AlbumSettings.java create mode 100644 src/com/android/dreams/phototable/FlipperDreamSettings.java create mode 100644 src/com/android/dreams/phototable/PhotoTableDream.java create mode 100644 src/com/android/dreams/phototable/PhotoTableDreamSettings.java delete mode 100644 src/com/android/dreams/phototable/Table.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4511728..2e6c39c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -10,26 +10,47 @@ android:icon="@mipmap/icon" android:largeHeap="true"> android:hardwareAccelerated="true" - + + + + + + + - android:icon="@mipmap/flip" + + + + + + + diff --git a/res/layout/album.xml b/res/layout/album.xml new file mode 100644 index 0000000..890d96c --- /dev/null +++ b/res/layout/album.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/res/values/config.xml b/res/values/config.xml index 8039f28..1305cf7 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -52,5 +52,9 @@ 950000 + + + 100 + diff --git a/res/values/ids.xml b/res/values/ids.xml index 3f65a0c..4159edc 100644 --- a/res/values/ids.xml +++ b/res/values/ids.xml @@ -17,4 +17,5 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index 897f5b2..83de506 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -17,4 +17,8 @@ Photo Screensavers Photo Table Photo Flipper + Photos from Posts + Unnamed Album + Stock Photos + https://lh4.googleusercontent.com/-Wd4RWlOXkSo/STt6r8uEJBI/AAAAAAAABJk/2An3QS-aD5c/s256/044_002.jpg diff --git a/src/com/android/dreams/phototable/AlbumDataAdapter.java b/src/com/android/dreams/phototable/AlbumDataAdapter.java new file mode 100644 index 0000000..3d68627 --- /dev/null +++ b/src/com/android/dreams/phototable/AlbumDataAdapter.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2012 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.dreams.phototable; + +import android.content.SharedPreferences; +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Settings panel for photo flipping dream. + */ +public class AlbumDataAdapter extends ArrayAdapter { + private static final String TAG = "AlbumDataAdapter"; + + public static final String ALBUM_SET = "Enabled Album Set"; + + private final SharedPreferences mSettings; + private final LayoutInflater mInflater; + private final int mLayout; + private final ItemClickListener mListener; + + private Set mEnabledAlbums; + + public AlbumDataAdapter(Context context, SharedPreferences settings, + int resource, List objects) { + super(context, resource, objects); + mSettings = settings; + mLayout = resource; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mListener = new ItemClickListener(); + mEnabledAlbums = AlbumSettings.getEnabledAlbums(mSettings); + } + + @Override + public View getView (int position, View convertView, ViewGroup parent) { + View item = convertView; + if (item == null) { + item = mInflater.inflate(mLayout, parent, false); + } else { + } + PhotoSource.AlbumData data = getItem(position); + + View vCheckBox = item.findViewById(R.id.enabled); + if (vCheckBox != null && vCheckBox instanceof CheckBox) { + CheckBox checkBox = (CheckBox) vCheckBox; + checkBox.setOnCheckedChangeListener(null); + checkBox.setChecked(mEnabledAlbums.contains(data.id)); + checkBox.setText(data.toString()); + checkBox.setTag(R.id.data_payload, data); + checkBox.setOnCheckedChangeListener(mListener); + } + + return item; + } + + public static class RecencyComparator implements Comparator { + @Override + public int compare(PhotoSource.AlbumData a, PhotoSource.AlbumData b) { + if (a.updated == b.updated) { + return a.title.compareTo(b.title); + } else { + return (int) Math.signum(b.updated - a.updated); + } + } + } + + public static class AlphabeticalComparator implements Comparator { + @Override + public int compare(PhotoSource.AlbumData a, PhotoSource.AlbumData b) { + return a.title.compareTo(b.title); + } + } + + private class ItemClickListener implements CompoundButton.OnCheckedChangeListener { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + PhotoSource.AlbumData data = + (PhotoSource.AlbumData) buttonView.getTag(R.id.data_payload); + + if (isChecked) { + mEnabledAlbums.add(data.id); + } else { + mEnabledAlbums.remove(data.id); + } + + AlbumSettings.setEnabledAlbums(mSettings , mEnabledAlbums); + + Log.i("adaptor", data.title + " is " + (isChecked ? "" : "not") + " enabled"); + } + } +} diff --git a/src/com/android/dreams/phototable/AlbumSettings.java b/src/com/android/dreams/phototable/AlbumSettings.java new file mode 100644 index 0000000..62a0162 --- /dev/null +++ b/src/com/android/dreams/phototable/AlbumSettings.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2012 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.dreams.phototable; + +import android.content.SharedPreferences; + +import java.util.HashSet; +import java.util.Set; + +/** + * Common utilities for album settings. + */ +public class AlbumSettings { + public static final String ALBUM_SET = "Enabled Album Set"; + + public static Set getEnabledAlbums(SharedPreferences settings) { + Set enabled = settings.getStringSet(ALBUM_SET, null); + if (enabled == null) { + enabled= new HashSet(); + enabled.add(StockSource.ALBUM_ID); + setEnabledAlbums(settings, enabled); + } + return enabled; + } + + public static void setEnabledAlbums(SharedPreferences settings, Set value) { + SharedPreferences.Editor editor = settings.edit(); + editor.putStringSet(ALBUM_SET, value); + editor.commit(); + } +} diff --git a/src/com/android/dreams/phototable/FlipperDream.java b/src/com/android/dreams/phototable/FlipperDream.java index f7b1cfe..f2e20b9 100644 --- a/src/com/android/dreams/phototable/FlipperDream.java +++ b/src/com/android/dreams/phototable/FlipperDream.java @@ -21,7 +21,7 @@ import android.service.dreams.Dream; * Example interactive screen saver: single photo with flipping. */ public class FlipperDream extends Dream { - private static final String TAG = "FlipperDream"; + public static final String TAG = "FlipperDream"; @Override public void onStart() { diff --git a/src/com/android/dreams/phototable/FlipperDreamSettings.java b/src/com/android/dreams/phototable/FlipperDreamSettings.java new file mode 100644 index 0000000..0ae20df --- /dev/null +++ b/src/com/android/dreams/phototable/FlipperDreamSettings.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 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.dreams.phototable; + +import android.content.SharedPreferences; +import android.app.ListActivity; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import java.util.LinkedList; + +/** + * Settings panel for photo flipping dream. + */ +public class FlipperDreamSettings extends ListActivity { + private static final String TAG = "FlipperDreamSettings"; + public static final String PREFS_NAME = FlipperDream.TAG; + + private PhotoSourcePlexor mPhotoSource; + private ArrayAdapter mAdapter; + private SharedPreferences mSettings; + + @Override + protected void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + + //setContentView(R.layout.custom_list_activity_view); + + mSettings = getSharedPreferences(PREFS_NAME, 0); + + mPhotoSource = new PhotoSourcePlexor(this, mSettings); + mAdapter = new AlbumDataAdapter(this, + mSettings, + R.layout.album, + new LinkedList(mPhotoSource.findAlbums())); + mAdapter.sort(new AlbumDataAdapter.RecencyComparator()); + setListAdapter(mAdapter); + } +} diff --git a/src/com/android/dreams/phototable/LocalSource.java b/src/com/android/dreams/phototable/LocalSource.java index 8dbc079..ec23b6a 100644 --- a/src/com/android/dreams/phototable/LocalSource.java +++ b/src/com/android/dreams/phototable/LocalSource.java @@ -16,12 +16,14 @@ package com.android.dreams.phototable; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.provider.MediaStore; import java.io.FileInputStream; import java.io.InputStream; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; /** @@ -32,15 +34,71 @@ public class LocalSource extends PhotoSource { private int mNextPosition; - public static final int TYPE = 2; - - public LocalSource(Context context) { - super(context); + public LocalSource(Context context, SharedPreferences settings) { + super(context, settings); mSourceName = TAG; mNextPosition = -1; fillQueue(); } + @Override + public Collection findAlbums() { + log(TAG, "finding albums"); + HashMap foundAlbums = new HashMap(); + + String[] projection = {MediaStore.Images.Media.DATA, MediaStore.Images.Media.BUCKET_ID, + MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.DATE_TAKEN}; + // This is a horrible hack that closes the where clause and injects a grouping clause. + Cursor cursor = mResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + + int dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + int bucketIndex = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID); + int nameIndex = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME); + int updatedIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN); + + if (bucketIndex < 0) { + log(TAG, "can't find the ID column!"); + } else { + while (!cursor.isAfterLast()) { + String id = TAG + ":" + cursor.getString(bucketIndex); + AlbumData data = foundAlbums.get(id); + if (foundAlbums.get(id) == null) { + data = new AlbumData(); + data.id = id; + + if (dataIndex >= 0) { + data.thumbnailUrl = cursor.getString(dataIndex); + } + + if (nameIndex >= 0) { + data.title = cursor.getString(nameIndex); + } else { + data.title = + mResources.getString(R.string.unknown_album_name, "Unknown"); + } + + log(TAG, data.title + " found"); + foundAlbums.put(id, data); + } + if (updatedIndex >= 0) { + long updated = cursor.getLong(updatedIndex); + data.updated = (data.updated == 0 ? + updated : + Math.min(data.updated, updated)); + } + cursor.moveToNext(); + } + } + cursor.close(); + + } + log(TAG, "found " + foundAlbums.size() + " items."); + return foundAlbums.values(); + } + @Override protected Collection findImages(int howMany) { log(TAG, "finding images"); @@ -48,14 +106,22 @@ public class LocalSource extends PhotoSource { String[] projection = {MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION, MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.BUCKET_DISPLAY_NAME}; - String[] selectionArgs = {}; // settings go here String selection = ""; - for (String arg : selectionArgs) { - if (selection.length() > 0) { - selection += " OR "; + for (String id : AlbumSettings.getEnabledAlbums(mSettings)) { + if (id.startsWith(TAG)) { + String[] parts = id.split(":"); + if (parts.length > 1) { + if (selection.length() > 0) { + selection += " OR "; + } + selection += MediaStore.Images.Media.BUCKET_ID + " = '" + parts[1] + "'"; + } } - selection += MediaStore.Images.Media.BUCKET_ID + " = '" + arg + "'"; } + if (selection.isEmpty()) { + return foundImages; + } + Cursor cursor = mResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, null, null); if (cursor != null) { @@ -77,7 +143,6 @@ public class LocalSource extends PhotoSource { } else { while (foundImages.size() < howMany && !cursor.isAfterLast()) { ImageData data = new ImageData(); - data.type = TYPE; data.url = cursor.getString(dataIndex); data.orientation = cursor.getInt(orientationIndex); foundImages.offer(data); diff --git a/src/com/android/dreams/phototable/PhotoCarousel.java b/src/com/android/dreams/phototable/PhotoCarousel.java index 838af18..99804d5 100644 --- a/src/com/android/dreams/phototable/PhotoCarousel.java +++ b/src/com/android/dreams/phototable/PhotoCarousel.java @@ -64,7 +64,8 @@ public class PhotoCarousel extends FrameLayout { mFlipDuration = resources.getInteger(R.integer.flip_duration); mOptions = new BitmapFactory.Options(); mOptions.inTempStorage = new byte[32768]; - mPhotoSource = new PhotoSourcePlexor(context); + mPhotoSource = new PhotoSourcePlexor(getContext(), + getContext().getSharedPreferences(FlipperDreamSettings.PREFS_NAME, 0)); mBitmapStore = new HashMap(); mPanel = new View[2]; @@ -89,11 +90,9 @@ public class PhotoCarousel extends FrameLayout { } private class PhotoLoadTask extends AsyncTask { - private int mTries; private ImageView mDestination; public PhotoLoadTask(View destination) { - mTries = 0; mDestination = (ImageView) destination; } @@ -114,9 +113,14 @@ public class PhotoCarousel extends FrameLayout { old.recycle(); } PhotoCarousel.this.requestLayout(); - } else if (mTries < 3) { - mTries++; - this.execute(); + } else { + postDelayed(new Runnable() { + @Override + public void run() { + new PhotoLoadTask(mDestination) + .execute(); + } + }, 100); } } }; diff --git a/src/com/android/dreams/phototable/PhotoSource.java b/src/com/android/dreams/phototable/PhotoSource.java index 2851c6c..32c7739 100644 --- a/src/com/android/dreams/phototable/PhotoSource.java +++ b/src/com/android/dreams/phototable/PhotoSource.java @@ -17,6 +17,7 @@ package com.android.dreams.phototable; import android.content.ContentResolver; import android.content.Context; +import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; @@ -46,25 +47,43 @@ public abstract class PhotoSource { // that we can mark and reset the input stream to avoid duplicate network i/o private static final int BUFFER_SIZE = 128 * 1024; - public static class ImageData { + public class ImageData { public String id; public String url; public int orientation; - public int type; + + InputStream getStream() { + return PhotoSource.this.getStream(this); + } + } + + public static class AlbumData { + public String id; + public String title; + public String thumbnailUrl; + public long updated; + + @Override + public String toString() { + return title; + } } - private final Context mContext; private final LinkedList mImageQueue; private final int mMaxQueueSize; + protected final Context mContext; protected final Resources mResources; - protected ContentResolver mResolver; - protected String mSourceName; protected final Random mRNG; + protected final SharedPreferences mSettings; + protected final ContentResolver mResolver; + + protected String mSourceName; - public PhotoSource(Context context) { + public PhotoSource(Context context, SharedPreferences settings) { mSourceName = TAG; mContext = context; + mSettings = settings; mResolver = mContext.getContentResolver(); mResources = context.getResources(); mImageQueue = new LinkedList(); @@ -91,7 +110,7 @@ public abstract class PhotoSource { ImageData data = mImageQueue.poll(); InputStream is = null; try { - is = getStream(data); + is = data.getStream(); BufferedInputStream bis = new BufferedInputStream(is); bis.mark(BUFFER_SIZE); @@ -180,4 +199,5 @@ public abstract class PhotoSource { protected abstract InputStream getStream(ImageData data); protected abstract Collection findImages(int howMany); + public abstract Collection findAlbums(); } diff --git a/src/com/android/dreams/phototable/PhotoSourcePlexor.java b/src/com/android/dreams/phototable/PhotoSourcePlexor.java index 4c4550e..7cf207e 100644 --- a/src/com/android/dreams/phototable/PhotoSourcePlexor.java +++ b/src/com/android/dreams/phototable/PhotoSourcePlexor.java @@ -16,6 +16,7 @@ package com.android.dreams.phototable; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; @@ -34,13 +35,33 @@ public class PhotoSourcePlexor extends PhotoSource { private final PhotoSource mPicasaSource; private final PhotoSource mLocalSource; private final PhotoSource mStockSource; + private SharedPreferences mSettings; - public PhotoSourcePlexor(Context context) { - super(context); + public PhotoSourcePlexor(Context context, SharedPreferences settings) { + super(context, settings); mSourceName = TAG; - mPicasaSource = new PicasaSource(context); - mLocalSource = new LocalSource(context); - mStockSource = new StockSource(context); + mPicasaSource = new PicasaSource(context, settings); + mLocalSource = new LocalSource(context, settings); + mStockSource = new StockSource(context, settings); + } + + @Override + public Collection findAlbums() { + log(TAG, "finding albums"); + LinkedList foundAlbums = new LinkedList(); + + foundAlbums.addAll(mPicasaSource.findAlbums()); + log(TAG, "found " + foundAlbums.size() + " network albums"); + + foundAlbums.addAll(mLocalSource.findAlbums()); + log(TAG, "found " + foundAlbums.size() + " user albums"); + + if (foundAlbums.isEmpty()) { + foundAlbums.addAll(mStockSource.findAlbums()); + } + log(TAG, "found " + foundAlbums.size() + " albums"); + + return foundAlbums; } @Override @@ -64,18 +85,6 @@ public class PhotoSourcePlexor extends PhotoSource { @Override protected InputStream getStream(ImageData data) { - switch (data.type) { - case PicasaSource.TYPE: - return mPicasaSource.getStream(data); - - case LocalSource.TYPE: - return mLocalSource.getStream(data); - - case StockSource.TYPE: - return mStockSource.getStream(data); - - default: - return null; - } + return data.getStream(); } } diff --git a/src/com/android/dreams/phototable/PhotoTable.java b/src/com/android/dreams/phototable/PhotoTable.java index 49c4b92..7e6faea 100644 --- a/src/com/android/dreams/phototable/PhotoTable.java +++ b/src/com/android/dreams/phototable/PhotoTable.java @@ -16,25 +16,431 @@ package com.android.dreams.phototable; import android.service.dreams.Dream; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.PointF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.AsyncTask; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; + +import java.util.LinkedList; +import java.util.Random; /** - * Example interactive screen saver: flick photos onto a table. + * A surface where photos sit. */ -public class PhotoTable extends Dream { +public class PhotoTable extends FrameLayout { private static final String TAG = "PhotoTable"; - private Table mTable; + private static final boolean DEBUG = false; + + class Launcher implements Runnable { + private final PhotoTable mTable; + public Launcher(PhotoTable table) { + mTable = table; + } + + @Override + public void run() { + mTable.scheduleNext(mDropPeriod); + mTable.launch(); + } + } + + private static final long MAX_SELECTION_TIME = 10000L; + private static Random sRNG = new Random(); + + private final Launcher mLauncher; + private final LinkedList mOnTable; + private final Dream mDream; + private final int mDropPeriod; + private final int mFastDropPeriod; + private final int mNowDropDelay; + private final float mImageRatio; + private final float mTableRatio; + private final float mImageRotationLimit; + private final boolean mTapToExit; + private final int mTableCapacity; + private final int mInset; + private final PhotoSourcePlexor mPhotoSource; + private final Resources mResources; + private PhotoLaunchTask mPhotoLaunchTask; + private boolean mStarted; + private boolean mIsLandscape; + private BitmapFactory.Options mOptions; + private int mLongSide; + private int mShortSide; + private int mWidth; + private int mHeight; + private View mSelected; + private long mSelectedTime; + + public PhotoTable(Dream dream, AttributeSet as) { + super(dream, as); + mDream = dream; + mResources = getResources(); + setBackground(mResources.getDrawable(R.drawable.table)); + mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset); + mDropPeriod = mResources.getInteger(R.integer.drop_period); + mFastDropPeriod = mResources.getInteger(R.integer.fast_drop); + mNowDropDelay = mResources.getInteger(R.integer.now_drop); + mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f; + mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f; + mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation); + mTableCapacity = mResources.getInteger(R.integer.table_capacity); + mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit); + mOnTable = new LinkedList(); + mOptions = new BitmapFactory.Options(); + mOptions.inTempStorage = new byte[32768]; + mPhotoSource = new PhotoSourcePlexor(getContext(), + getContext().getSharedPreferences(PhotoTableDreamSettings.PREFS_NAME, 0)); + mLauncher = new Launcher(this); + mStarted = false; + } + + public boolean hasSelection() { + return mSelected != null; + } + + public View getSelected() { + return mSelected; + } + + public void clearSelection() { + mSelected = null; + } + + public void setSelection(View selected) { + assert(selected != null); + if (mSelected != null) { + dropOnTable(mSelected); + } + mSelected = selected; + mSelectedTime = System.currentTimeMillis(); + bringChildToFront(selected); + pickUp(selected); + } + + static float lerp(float a, float b, float f) { + return (b-a)*f + a; + } + + static float randfrange(float a, float b) { + return lerp(a, b, sRNG.nextFloat()); + } + + static PointF randFromCurve(float t, PointF[] v) { + PointF p = new PointF(); + if (v.length == 4 && t >= 0f && t <= 1f) { + float a = (float) Math.pow(1f-t, 3f); + float b = (float) Math.pow(1f-t, 2f) * t; + float c = (1f-t) * (float) Math.pow(t, 2f); + float d = (float) Math.pow(t, 3f); + + p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x; + p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y; + } + return p; + } + + private static PointF randInCenter(float i, float j, int width, int height) { + log("randInCenter (" + i + ", " + j + ", " + width + ", " + height + ")"); + PointF p = new PointF(); + p.x = 0.5f * width + 0.15f * width * i; + p.y = 0.5f * height + 0.15f * height * j; + log("randInCenter returning " + p.x + "," + p.y); + return p; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (hasSelection()) { + dropOnTable(getSelected()); + clearSelection(); + } else { + if (mTapToExit) { + mDream.finish(); + } + } + return true; + } + return false; + } @Override - public void onStart() { - super.onStart(); - setInteractive(true); + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + log("onLayout (" + left + ", " + top + ", " + right + ", " + bottom + ")"); + + mHeight = bottom - top; + mWidth = right - left; + + mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight)); + mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight)); + + boolean isLandscape = mWidth > mHeight; + if (mIsLandscape != isLandscape) { + for (View photo: mOnTable) { + if (photo == getSelected()) { + pickUp(photo); + } else { + dropOnTable(photo); + } + } + mIsLandscape = isLandscape; + } + start(); } @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - mTable = new Table(this, null); - setContentView(mTable); - lightsOut(); + public boolean isOpaque() { + return true; + } + + private class PhotoLaunchTask extends AsyncTask { + @Override + public View doInBackground(Void... unused) { + log("load a new photo"); + final PhotoTable table = PhotoTable.this; + + LayoutInflater inflater = (LayoutInflater) table.getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View photo = inflater.inflate(R.layout.photo, null); + ImageView image = (ImageView) photo; + Drawable[] layers = new Drawable[2]; + Bitmap decodedPhoto = table.mPhotoSource.next(table.mOptions, + table.mLongSide, table.mShortSide); + int photoWidth = table.mOptions.outWidth; + int photoHeight = table.mOptions.outHeight; + if (table.mOptions.outWidth <= 0 || table.mOptions.outHeight <= 0) { + photo = null; + } else { + layers[0] = new BitmapDrawable(table.mResources, decodedPhoto); + layers[1] = table.mResources.getDrawable(R.drawable.frame); + LayerDrawable layerList = new LayerDrawable(layers); + layerList.setLayerInset(0, table.mInset, table.mInset, + table.mInset, table.mInset); + image.setImageDrawable(layerList); + + photo.setTag(R.id.photo_width, new Integer(photoWidth)); + photo.setTag(R.id.photo_height, new Integer(photoHeight)); + + photo.setOnTouchListener(new PhotoTouchListener(table.getContext(), + table)); + } + + return photo; + } + + @Override + public void onPostExecute(View photo) { + if (photo != null) { + final PhotoTable table = PhotoTable.this; + + table.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT)); + if (table.hasSelection()) { + table.bringChildToFront(table.getSelected()); + } + int width = ((Integer) photo.getTag(R.id.photo_width)).intValue(); + int height = ((Integer) photo.getTag(R.id.photo_height)).intValue(); + + log("drop it"); + table.throwOnTable(photo); + + if(table.mOnTable.size() < table.mTableCapacity) { + table.scheduleNext(table.mFastDropPeriod); + } + } + } + }; + + public void launch() { + log("launching"); + setSystemUiVisibility(View.STATUS_BAR_HIDDEN); + if (hasSelection() && + (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) { + dropOnTable(getSelected()); + clearSelection(); + } else { + log("inflate it"); + if (mPhotoLaunchTask == null || + mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) { + mPhotoLaunchTask = new PhotoLaunchTask(); + mPhotoLaunchTask.execute(); + } + } + } + public void fadeAway(final View photo, final boolean replace) { + // fade out of view + mOnTable.remove(photo); + photo.animate().cancel(); + photo.animate() + .withLayer() + .alpha(0f) + .setDuration(1000) + .withEndAction(new Runnable() { + @Override + public void run() { + removeView(photo); + recycle(photo); + if (replace) { + scheduleNext(mNowDropDelay); + } + } + }); + } + + public void moveToBackOfQueue(View photo) { + // make this photo the last to be removed. + bringChildToFront(photo); + invalidate(); + mOnTable.remove(photo); + mOnTable.offer(photo); + } + + private void throwOnTable(final View photo) { + mOnTable.offer(photo); + log("start offscreen"); + int width = ((Integer) photo.getTag(R.id.photo_width)); + int height = ((Integer) photo.getTag(R.id.photo_height)); + photo.setRotation(-100.0f); + photo.setX(-mLongSide); + photo.setY(-mLongSide); + dropOnTable(photo); + } + + public void dropOnTable(final View photo) { + float angle = randfrange(-mImageRotationLimit, mImageRotationLimit); + PointF p = randInCenter((float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(), + mWidth, mHeight); + float x = p.x; + float y = p.y; + + log("drop it at " + x + ", " + y); + + float x0 = photo.getX(); + float y0 = photo.getY(); + float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue(); + float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue(); + + x -= mTableRatio * mLongSide / 2f; + y -= mTableRatio * mLongSide / 2f; + log("fixed offset is " + x + ", " + y); + + float dx = x - x0; + float dy = y - y0; + + float dist = (float) (Math.sqrt(dx * dx + dy * dy)); + int duration = (int) (1000f * dist / 400f); + duration = Math.max(duration, 1000); + + log("animate it"); + // toss onto table + photo.animate() + .withLayer() + .scaleX(mTableRatio / mImageRatio) + .scaleY(mTableRatio / mImageRatio) + .rotation(angle) + .x(x) + .y(y) + .setDuration(duration) + .setInterpolator(new DecelerateInterpolator(3f)) + .withEndAction(new Runnable() { + @Override + public void run() { + while (mOnTable.size() > mTableCapacity) { + fadeAway(mOnTable.poll(), false); + } + } + }); + } + + /** wrap all orientations to the interval [-180, 180). */ + private float wrapAngle(float angle) { + float result = angle + 180; + result = ((result % 360) + 360) % 360; // catch negative numbers + result -= 180; + return result; + } + + private void pickUp(final View photo) { + float photoWidth = photo.getWidth(); + float photoHeight = photo.getHeight(); + + float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth); + + log("target it"); + float x = (getWidth() - photoWidth) / 2f; + float y = (getHeight() - photoHeight) / 2f; + + float x0 = photo.getX(); + float y0 = photo.getY(); + float dx = x - x0; + float dy = y - y0; + + float dist = (float) (Math.sqrt(dx * dx + dy * dy)); + int duration = (int) (1000f * dist / 1000f); + duration = Math.max(duration, 500); + + photo.setRotation(wrapAngle(photo.getRotation())); + + log("animate it"); + // toss onto table + photo.animate() + .withLayer() + .rotation(0f) + .scaleX(scale) + .scaleY(scale) + .x(x) + .y(y) + .setDuration(duration) + .setInterpolator(new DecelerateInterpolator(2f)) + .withEndAction(new Runnable() { + @Override + public void run() { + log("endtimes: " + photo.getX()); + } + }); + } + + private void recycle(View photo) { + ImageView image = (ImageView) photo; + LayerDrawable layers = (LayerDrawable) image.getDrawable(); + BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0); + bitmap.getBitmap().recycle(); + } + + public void start() { + if (!mStarted) { + log("kick it"); + mStarted = true; + scheduleNext(mDropPeriod); + launch(); + } + } + + public void scheduleNext(int delay) { + removeCallbacks(mLauncher); + postDelayed(mLauncher, delay); + } + + private static void log(String message) { + if (DEBUG) { + Log.i(TAG, message); + } } } diff --git a/src/com/android/dreams/phototable/PhotoTableDream.java b/src/com/android/dreams/phototable/PhotoTableDream.java new file mode 100644 index 0000000..47c672d --- /dev/null +++ b/src/com/android/dreams/phototable/PhotoTableDream.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 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.dreams.phototable; + +import android.service.dreams.Dream; + +/** + * Example interactive screen saver: flick photos onto a table. + */ +public class PhotoTableDream extends Dream { + public static final String TAG = "PhotoTableDream"; + private PhotoTable mTable; + + @Override + public void onStart() { + super.onStart(); + setInteractive(true); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + mTable = new PhotoTable(this, null); + setContentView(mTable); + lightsOut(); + } +} diff --git a/src/com/android/dreams/phototable/PhotoTableDreamSettings.java b/src/com/android/dreams/phototable/PhotoTableDreamSettings.java new file mode 100644 index 0000000..e7ba945 --- /dev/null +++ b/src/com/android/dreams/phototable/PhotoTableDreamSettings.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 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.dreams.phototable; + +import android.content.SharedPreferences; +import android.app.ListActivity; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import java.util.LinkedList; + +/** + * Settings panel for photo flipping dream. + */ +public class PhotoTableDreamSettings extends ListActivity { + private static final String TAG = "PhotoTableDreamSettings"; + public static final String PREFS_NAME = PhotoTableDream.TAG; + + private PhotoSourcePlexor mPhotoSource; + private ArrayAdapter mAdapter; + private SharedPreferences mSettings; + + @Override + protected void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + + //setContentView(R.layout.custom_list_activity_view); + + mSettings = getSharedPreferences(PREFS_NAME, 0); + + mPhotoSource = new PhotoSourcePlexor(this, mSettings); + mAdapter = new AlbumDataAdapter(this, + mSettings, + R.layout.album, + new LinkedList(mPhotoSource.findAlbums())); + mAdapter.sort(new AlbumDataAdapter.RecencyComparator()); + setListAdapter(mAdapter); + } +} diff --git a/src/com/android/dreams/phototable/PhotoTouchListener.java b/src/com/android/dreams/phototable/PhotoTouchListener.java index 439991a..2cfcbd4 100644 --- a/src/com/android/dreams/phototable/PhotoTouchListener.java +++ b/src/com/android/dreams/phototable/PhotoTouchListener.java @@ -34,7 +34,7 @@ public class PhotoTouchListener implements View.OnTouchListener { private static final int MAX_POINTER_COUNT = 10; private final int mTouchSlop; private final int mTapTimeout; - private final Table mTable; + private final PhotoTable mTable; private final float mBeta; private final float mTableRatio; private final boolean mEnableFling; @@ -56,7 +56,7 @@ public class PhotoTouchListener implements View.OnTouchListener { private float[] pts = new float[MAX_POINTER_COUNT]; private float[] tmp = new float[MAX_POINTER_COUNT]; - public PhotoTouchListener(Context context, Table table) { + public PhotoTouchListener(Context context, PhotoTable table) { mTable = table; final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); diff --git a/src/com/android/dreams/phototable/PicasaSource.java b/src/com/android/dreams/phototable/PicasaSource.java index 061b66b..4d1c677 100644 --- a/src/com/android/dreams/phototable/PicasaSource.java +++ b/src/com/android/dreams/phototable/PicasaSource.java @@ -16,6 +16,7 @@ package com.android.dreams.phototable; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; @@ -23,6 +24,7 @@ import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; /** @@ -31,23 +33,39 @@ import java.util.LinkedList; public class PicasaSource extends PhotoSource { private static final String TAG = "PhotoTable.PicasaSource"; + private static final String PICASA_AUTHORITY = + "com.google.android.gallery3d.GooglePhotoProvider"; + + private static final String PICASA_PHOTO_PATH = "photos"; + private static final String PICASA_ALBUM_PATH = "albums"; + private static final String PICASA_ID = "_id"; private static final String PICASA_URL = "content_url"; private static final String PICASA_ROTATION = "rotation"; private static final String PICASA_ALBUM_ID = "album_id"; + private static final String PICASA_TITLE = "title"; + private static final String PICASA_THUMB = "thumbnail_url"; + private static final String PICASA_ALBUM_TYPE = "album_type"; + private static final String PICASA_ALBUM_UPDATED = "date_updated"; private static final String PICASA_URL_KEY = "content_url"; private static final String PICASA_TYPE_KEY = "type"; - private static final String PICASA_TYPE_THUMB_VALUE = "full"; + private static final String PICASA_TYPE_FULL_VALUE = "full"; + private static final String PICASA_TYPE_SCREEN_VALUE = "screennail"; + private static final String PICASA_TYPE_THUMB_VALUE = "thumbnail"; + private static final String PICASA_TYPE_IMAGE_VALUE = "image"; + private static final String PICASA_BUZZ_TYPE = "Buzz"; - private int mNextPosition; + private final int mMaxPostAblums; - public static final int TYPE = 3; + private int mNextPosition; - public PicasaSource(Context context) { - super(context); + public PicasaSource(Context context, SharedPreferences settings) { + super(context, settings); mSourceName = TAG; mNextPosition = -1; + mMaxPostAblums = mResources.getInteger(R.integer.max_post_albums); + log(TAG, "mSettings: " + mSettings); fillQueue(); } @@ -56,20 +74,45 @@ public class PicasaSource extends PhotoSource { log(TAG, "finding images"); LinkedList foundImages = new LinkedList(); String[] projection = {PICASA_ID, PICASA_URL, PICASA_ROTATION, PICASA_ALBUM_ID}; - String[] selectionArgs = {}; // settings go here - String selection = ""; - for (String arg : selectionArgs) { - if (selection.length() > 0) { - selection += " OR "; + StringBuilder selection = new StringBuilder(); + boolean usePosts = false; + for (String id : AlbumSettings.getEnabledAlbums(mSettings)) { + if (id.startsWith(TAG)) { + String[] parts = id.split(":"); + if (parts.length > 1) { + if (selection.length() > 0) { + selection.append(" OR "); + } + if (PICASA_BUZZ_TYPE.equals(parts[1])) { + usePosts = true; + } else { + log(TAG, "adding on: " + parts[1]); + selection.append(PICASA_ALBUM_ID + " = '" + parts[1] + "'"); + } + } } - selection += PICASA_ALBUM_ID + " = '" + arg + "'"; } + if (usePosts) { + for (String id : findPostIds()) { + if (selection.length() > 0) { + selection.append(" OR "); + } + log(TAG, "adding on: " + id); + selection.append(PICASA_ALBUM_ID + " = '" + id + "'"); + } + } + + if (selection.length() == 0) { + return foundImages; + } + log(TAG, "selection is: " + selection.toString()); + Uri.Builder picasaUriBuilder = new Uri.Builder() .scheme("content") - .authority("com.google.android.gallery3d.GooglePhotoProvider") - .appendPath("photos"); + .authority(PICASA_AUTHORITY) + .appendPath(PICASA_PHOTO_PATH); Cursor cursor = mResolver.query(picasaUriBuilder.build(), - projection, selection, null, null); + projection, selection.toString(), null, null); if (cursor != null) { if (cursor.getCount() > howMany && mNextPosition == -1) { mNextPosition = @@ -92,7 +135,6 @@ public class PicasaSource extends PhotoSource { while (foundImages.size() < howMany && !cursor.isAfterLast()) { if (idIndex >= 0) { ImageData data = new ImageData(); - data.type = TYPE; data.id = cursor.getString(idIndex); if (urlIndex >= 0) { @@ -116,16 +158,118 @@ public class PicasaSource extends PhotoSource { return foundImages; } + private Collection findPostIds() { + LinkedList postIds = new LinkedList(); + String[] projection = {PICASA_ID, PICASA_ALBUM_TYPE, PICASA_ALBUM_UPDATED}; + String order = PICASA_ALBUM_UPDATED + " DESC"; + Uri.Builder picasaUriBuilder = new Uri.Builder() + .scheme("content") + .authority(PICASA_AUTHORITY) + .appendPath(PICASA_ALBUM_PATH) + .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_IMAGE_VALUE); + Cursor cursor = mResolver.query(picasaUriBuilder.build(), + projection, null, null, order); + if (cursor != null) { + cursor.moveToFirst(); + + int idIndex = cursor.getColumnIndex(PICASA_ID); + int typeIndex = cursor.getColumnIndex(PICASA_ALBUM_TYPE); + + if (idIndex < 0) { + log(TAG, "can't find the ID column!"); + } else { + while (postIds.size() < mMaxPostAblums && !cursor.isAfterLast()) { + if (typeIndex >= 0 && PICASA_BUZZ_TYPE.equals(cursor.getString(typeIndex))) { + postIds.add(cursor.getString(idIndex)); + } + cursor.moveToNext(); + } + } + cursor.close(); + } + return postIds; + } + + @Override + public Collection findAlbums() { + log(TAG, "finding albums"); + HashMap foundAlbums = new HashMap(); + String[] projection = {PICASA_ID, PICASA_TITLE, PICASA_THUMB, PICASA_ALBUM_TYPE, + PICASA_ALBUM_UPDATED}; + Uri.Builder picasaUriBuilder = new Uri.Builder() + .scheme("content") + .authority(PICASA_AUTHORITY) + .appendPath(PICASA_ALBUM_PATH) + .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_IMAGE_VALUE); + Cursor cursor = mResolver.query(picasaUriBuilder.build(), + projection, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + + int idIndex = cursor.getColumnIndex(PICASA_ID); + int thumbIndex = cursor.getColumnIndex(PICASA_THUMB); + int titleIndex = cursor.getColumnIndex(PICASA_TITLE); + int typeIndex = cursor.getColumnIndex(PICASA_ALBUM_TYPE); + int updatedIndex = cursor.getColumnIndex(PICASA_ALBUM_UPDATED); + + if (idIndex < 0) { + log(TAG, "can't find the ID column!"); + } else { + while (!cursor.isAfterLast()) { + String id = TAG + ":" + cursor.getString(idIndex); + boolean isBuzz = (typeIndex >= 0 && + PICASA_BUZZ_TYPE.equals(cursor.getString(typeIndex))); + + if (isBuzz) { + id = TAG + ":" + PICASA_BUZZ_TYPE; + } + + if (foundAlbums.get(id) == null) { + AlbumData data = new AlbumData(); + data.id = id; + + if (isBuzz) { + data.title = + mResources.getString(R.string.posts_album_name, "Posts"); + } else if (titleIndex >= 0) { + data.title = cursor.getString(titleIndex); + } else { + data.title = + mResources.getString(R.string.unknown_album_name, "Unknown"); + } + + if (updatedIndex >= 0) { + data.updated = cursor.getLong(updatedIndex); + } + + if (thumbIndex >= 0) { + data.thumbnailUrl = cursor.getString(thumbIndex); + } + + log(TAG, "found " + data.title + "(" + data.id + ")"); + foundAlbums.put(id, data); + } + + cursor.moveToNext(); + } + } + cursor.close(); + + } + log(TAG, "found " + foundAlbums.size() + " items."); + return foundAlbums.values(); + } + @Override protected InputStream getStream(ImageData data) { InputStream is = null; try { Uri.Builder photoUriBuilder = new Uri.Builder() .scheme("content") - .authority("com.google.android.gallery3d.GooglePhotoProvider") - .appendPath("photos") + .authority(PICASA_AUTHORITY) + .appendPath(PICASA_PHOTO_PATH) .appendPath(data.id) - .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_THUMB_VALUE); + .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_FULL_VALUE); if (data.url != null) { photoUriBuilder.appendQueryParameter(PICASA_URL_KEY, data.url); } diff --git a/src/com/android/dreams/phototable/StockSource.java b/src/com/android/dreams/phototable/StockSource.java index 63e834b..d7c6d23 100644 --- a/src/com/android/dreams/phototable/StockSource.java +++ b/src/com/android/dreams/phototable/StockSource.java @@ -16,6 +16,7 @@ package com.android.dreams.phototable; import android.content.Context; +import android.content.SharedPreferences; import android.util.Log; import java.io.InputStream; @@ -26,6 +27,7 @@ import java.util.LinkedList; * Picks a random image from the local store. */ public class StockSource extends PhotoSource { + public static final String ALBUM_ID = "com.android.dreams.phototable.StockSource"; private static final String TAG = "PhotoTable.StockSource"; private static final int[] PHOTOS = {R.drawable.photo_044_002, R.drawable.photo_039_002, @@ -42,23 +44,36 @@ public class StockSource extends PhotoSource { }; private final LinkedList mImageList; - private int mNextPosition; + private final LinkedList mAlbumList; - public static final int TYPE = 1; + private int mNextPosition; - public StockSource(Context context) { - super(context); + public StockSource(Context context, SharedPreferences settings) { + super(context, settings); mSourceName = TAG; mImageList = new LinkedList(); + mAlbumList = new LinkedList(); fillQueue(); } + @Override + public Collection findAlbums() { + if (mAlbumList.isEmpty()) { + AlbumData data = new AlbumData(); + data.id = ALBUM_ID; + data.title = mResources.getString(R.string.stock_photo_album_name, "Default Photos"); + data.thumbnailUrl = mResources.getString(R.string.stock_photo_thumbnail_url); + mAlbumList.offer(data); + } + log(TAG, "returning a list of albums: " + mAlbumList.size()); + return mAlbumList; + } + @Override protected Collection findImages(int howMany) { if (mImageList.isEmpty()) { for (int i = 0; i < PHOTOS.length; i++) { ImageData data = new ImageData(); - data.type = TYPE; data.id = Integer.toString(PHOTOS[i]); mImageList.offer(data); } diff --git a/src/com/android/dreams/phototable/Table.java b/src/com/android/dreams/phototable/Table.java deleted file mode 100644 index d673175..0000000 --- a/src/com/android/dreams/phototable/Table.java +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Copyright (C) 2012 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.dreams.phototable; - -import android.service.dreams.Dream; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.PointF; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.os.AsyncTask; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.animation.DecelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.FrameLayout.LayoutParams; -import android.widget.ImageView; - -import java.util.LinkedList; -import java.util.Random; - -/** - * A surface where photos sit. - */ -public class Table extends FrameLayout { - private static final String TAG = "PhotoTable.Table"; - private static final boolean DEBUG = false; - - class Launcher implements Runnable { - private final Table mTable; - public Launcher(Table table) { - mTable = table; - } - - @Override - public void run() { - mTable.scheduleNext(mDropPeriod); - mTable.launch(); - } - } - - private static final long MAX_SELECTION_TIME = 10000L; - private static Random sRNG = new Random(); - - private final Launcher mLauncher; - private final LinkedList mOnTable; - private final Dream mDream; - private final int mDropPeriod; - private final int mFastDropPeriod; - private final int mNowDropDelay; - private final float mImageRatio; - private final float mTableRatio; - private final float mImageRotationLimit; - private final boolean mTapToExit; - private final int mTableCapacity; - private final int mInset; - private final PhotoSourcePlexor mPhotoSource; - private final Resources mResources; - private PhotoLaunchTask mPhotoLaunchTask; - private boolean mStarted; - private boolean mIsLandscape; - private BitmapFactory.Options mOptions; - private int mLongSide; - private int mShortSide; - private int mWidth; - private int mHeight; - private View mSelected; - private long mSelectedTime; - - public Table(Dream dream, AttributeSet as) { - super(dream, as); - mDream = dream; - mResources = getResources(); - setBackground(mResources.getDrawable(R.drawable.table)); - mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset); - mDropPeriod = mResources.getInteger(R.integer.drop_period); - mFastDropPeriod = mResources.getInteger(R.integer.fast_drop); - mNowDropDelay = mResources.getInteger(R.integer.now_drop); - mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f; - mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f; - mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation); - mTableCapacity = mResources.getInteger(R.integer.table_capacity); - mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit); - mOnTable = new LinkedList(); - mOptions = new BitmapFactory.Options(); - mOptions.inTempStorage = new byte[32768]; - mPhotoSource = new PhotoSourcePlexor(getContext()); - mLauncher = new Launcher(this); - mStarted = false; - } - - public boolean hasSelection() { - return mSelected != null; - } - - public View getSelected() { - return mSelected; - } - - public void clearSelection() { - mSelected = null; - } - - public void setSelection(View selected) { - assert(selected != null); - if (mSelected != null) { - dropOnTable(mSelected); - } - mSelected = selected; - mSelectedTime = System.currentTimeMillis(); - bringChildToFront(selected); - pickUp(selected); - } - - static float lerp(float a, float b, float f) { - return (b-a)*f + a; - } - - static float randfrange(float a, float b) { - return lerp(a, b, sRNG.nextFloat()); - } - - static PointF randFromCurve(float t, PointF[] v) { - PointF p = new PointF(); - if (v.length == 4 && t >= 0f && t <= 1f) { - float a = (float) Math.pow(1f-t, 3f); - float b = (float) Math.pow(1f-t, 2f) * t; - float c = (1f-t) * (float) Math.pow(t, 2f); - float d = (float) Math.pow(t, 3f); - - p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x; - p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y; - } - return p; - } - - private static PointF randInCenter(float i, float j, int width, int height) { - log("randInCenter (" + i + ", " + j + ", " + width + ", " + height + ")"); - PointF p = new PointF(); - p.x = 0.5f * width + 0.15f * width * i; - p.y = 0.5f * height + 0.15f * height * j; - log("randInCenter returning " + p.x + "," + p.y); - return p; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - if (hasSelection()) { - dropOnTable(getSelected()); - clearSelection(); - } else { - if (mTapToExit) { - mDream.finish(); - } - } - return true; - } - return false; - } - - @Override - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - log("onLayout (" + left + ", " + top + ", " + right + ", " + bottom + ")"); - - mHeight = bottom - top; - mWidth = right - left; - - mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight)); - mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight)); - - boolean isLandscape = mWidth > mHeight; - if (mIsLandscape != isLandscape) { - for (View photo: mOnTable) { - if (photo == getSelected()) { - pickUp(photo); - } else { - dropOnTable(photo); - } - } - mIsLandscape = isLandscape; - } - start(); - } - - @Override - public boolean isOpaque() { - return true; - } - - private class PhotoLaunchTask extends AsyncTask { - private int mTries; - public PhotoLaunchTask() { - mTries = 0; - } - - @Override - public View doInBackground(Void... unused) { - log("load a new photo"); - LayoutInflater inflater = (LayoutInflater) Table.this.getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View photo = inflater.inflate(R.layout.photo, null); - ImageView image = (ImageView) photo; - Drawable[] layers = new Drawable[2]; - Bitmap decodedPhoto = Table.this.mPhotoSource.next(Table.this.mOptions, - Table.this.mLongSide, Table.this.mShortSide); - int photoWidth = Table.this.mOptions.outWidth; - int photoHeight = Table.this.mOptions.outHeight; - if (Table.this.mOptions.outWidth <= 0 || Table.this.mOptions.outHeight <= 0) { - photo = null; - } else { - layers[0] = new BitmapDrawable(Table.this.mResources, decodedPhoto); - layers[1] = Table.this.mResources.getDrawable(R.drawable.frame); - LayerDrawable layerList = new LayerDrawable(layers); - layerList.setLayerInset(0, Table.this.mInset, Table.this.mInset, - Table.this.mInset, Table.this.mInset); - image.setImageDrawable(layerList); - - photo.setTag(R.id.photo_width, new Integer(photoWidth)); - photo.setTag(R.id.photo_height, new Integer(photoHeight)); - - photo.setOnTouchListener(new PhotoTouchListener(Table.this.getContext(), - Table.this)); - } - - return photo; - } - - @Override - public void onPostExecute(View photo) { - if (photo != null) { - Table.this.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT)); - if (Table.this.hasSelection()) { - Table.this.bringChildToFront(Table.this.getSelected()); - } - int width = ((Integer) photo.getTag(R.id.photo_width)).intValue(); - int height = ((Integer) photo.getTag(R.id.photo_height)).intValue(); - - log("drop it"); - Table.this.throwOnTable(photo); - - if(Table.this.mOnTable.size() < Table.this.mTableCapacity) { - Table.this.scheduleNext(Table.this.mFastDropPeriod); - } - } else if (mTries < 3) { - mTries++; - this.execute(); - } - } - }; - - public void launch() { - log("launching"); - setSystemUiVisibility(View.STATUS_BAR_HIDDEN); - if (hasSelection() && - (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) { - dropOnTable(getSelected()); - clearSelection(); - } else { - log("inflate it"); - if (mPhotoLaunchTask == null || - mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) { - mPhotoLaunchTask = new PhotoLaunchTask(); - mPhotoLaunchTask.execute(); - } - } - } - public void fadeAway(final View photo, final boolean replace) { - // fade out of view - mOnTable.remove(photo); - photo.animate().cancel(); - photo.animate() - .withLayer() - .alpha(0f) - .setDuration(1000) - .withEndAction(new Runnable() { - @Override - public void run() { - removeView(photo); - recycle(photo); - if (replace) { - scheduleNext(mNowDropDelay); - } - } - }); - } - - public void moveToBackOfQueue(View photo) { - // make this photo the last to be removed. - bringChildToFront(photo); - invalidate(); - mOnTable.remove(photo); - mOnTable.offer(photo); - } - - private void throwOnTable(final View photo) { - mOnTable.offer(photo); - log("start offscreen"); - int width = ((Integer) photo.getTag(R.id.photo_width)); - int height = ((Integer) photo.getTag(R.id.photo_height)); - photo.setRotation(-100.0f); - photo.setX(-mLongSide); - photo.setY(-mLongSide); - dropOnTable(photo); - } - - public void dropOnTable(final View photo) { - float angle = randfrange(-mImageRotationLimit, mImageRotationLimit); - PointF p = randInCenter((float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(), - mWidth, mHeight); - float x = p.x; - float y = p.y; - - log("drop it at " + x + ", " + y); - - float x0 = photo.getX(); - float y0 = photo.getY(); - float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue(); - float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue(); - - x -= mTableRatio * mLongSide / 2f; - y -= mTableRatio * mLongSide / 2f; - log("fixed offset is " + x + ", " + y); - - float dx = x - x0; - float dy = y - y0; - - float dist = (float) (Math.sqrt(dx * dx + dy * dy)); - int duration = (int) (1000f * dist / 400f); - duration = Math.max(duration, 1000); - - log("animate it"); - // toss onto table - photo.animate() - .withLayer() - .scaleX(mTableRatio / mImageRatio) - .scaleY(mTableRatio / mImageRatio) - .rotation(angle) - .x(x) - .y(y) - .setDuration(duration) - .setInterpolator(new DecelerateInterpolator(3f)) - .withEndAction(new Runnable() { - @Override - public void run() { - while (mOnTable.size() > mTableCapacity) { - fadeAway(mOnTable.poll(), false); - } - } - }); - } - - /** wrap all orientations to the interval [-180, 180). */ - private float wrapAngle(float angle) { - float result = angle + 180; - result = ((result % 360) + 360) % 360; // catch negative numbers - result -= 180; - return result; - } - - private void pickUp(final View photo) { - float photoWidth = photo.getWidth(); - float photoHeight = photo.getHeight(); - - float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth); - - log("target it"); - float x = (getWidth() - photoWidth) / 2f; - float y = (getHeight() - photoHeight) / 2f; - - float x0 = photo.getX(); - float y0 = photo.getY(); - float dx = x - x0; - float dy = y - y0; - - float dist = (float) (Math.sqrt(dx * dx + dy * dy)); - int duration = (int) (1000f * dist / 1000f); - duration = Math.max(duration, 500); - - photo.setRotation(wrapAngle(photo.getRotation())); - - log("animate it"); - // toss onto table - photo.animate() - .withLayer() - .rotation(0f) - .scaleX(scale) - .scaleY(scale) - .x(x) - .y(y) - .setDuration(duration) - .setInterpolator(new DecelerateInterpolator(2f)) - .withEndAction(new Runnable() { - @Override - public void run() { - log("endtimes: " + photo.getX()); - } - }); - } - - private void recycle(View photo) { - ImageView image = (ImageView) photo; - LayerDrawable layers = (LayerDrawable) image.getDrawable(); - BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0); - bitmap.getBitmap().recycle(); - } - - public void start() { - if (!mStarted) { - log("kick it"); - mStarted = true; - scheduleNext(mDropPeriod); - launch(); - } - } - - public void scheduleNext(int delay) { - removeCallbacks(mLauncher); - postDelayed(mLauncher, delay); - } - - private static void log(String message) { - if (DEBUG) { - Log.i(TAG, message); - } - } -} -- cgit v1.2.3