summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris Wren <cwren@android.com>2012-08-16 17:33:34 -0400
committerThe Android Automerger <android-build@android.com>2012-08-23 14:22:16 -0700
commitced88667b08756f009f6c0c15f2c0c2cd1dace8b (patch)
tree0b708e6bea60854beaf9f4184d857b8ca2bff7e4
parent7a809b2eda2d9cf8439202e03a092e850bfde1dc (diff)
downloadandroid_packages_screensavers_PhotoTable-ced88667b08756f009f6c0c15f2c0c2cd1dace8b.tar.gz
android_packages_screensavers_PhotoTable-ced88667b08756f009f6c0c15f2c0c2cd1dace8b.tar.bz2
android_packages_screensavers_PhotoTable-ced88667b08756f009f6c0c15f2c0c2cd1dace8b.zip
Example Dream: photos
An example screensaver that shows local photos. Change-Id: I2997c4f8ab35777d96df31a46f2f55044e3114c2
-rw-r--r--Android.mk19
-rw-r--r--AndroidManifest.xml22
-rw-r--r--res/anim/slideshow_in.xml5
-rw-r--r--res/anim/slideshow_out.xml7
-rw-r--r--res/drawable-hdpi/icon.pngbin0 -> 4713 bytes
-rw-r--r--res/drawable-ldpi/icon.pngbin0 -> 1707 bytes
-rw-r--r--res/drawable-mdpi/icon.pngbin0 -> 2850 bytes
-rw-r--r--res/drawable-nodpi/frame.9.pngbin0 -> 647 bytes
-rw-r--r--res/drawable-nodpi/photo_039_002.jpgbin0 -> 314891 bytes
-rw-r--r--res/drawable-nodpi/photo_044_002.jpgbin0 -> 252272 bytes
-rw-r--r--res/drawable-nodpi/photo_059_003.jpgbin0 -> 270403 bytes
-rw-r--r--res/drawable-nodpi/photo_070_004.jpgbin0 -> 324820 bytes
-rw-r--r--res/drawable-nodpi/photo_072_001.jpgbin0 -> 364562 bytes
-rw-r--r--res/drawable-nodpi/photo_077_002.jpgbin0 -> 330051 bytes
-rw-r--r--res/drawable-nodpi/photo_098_002.jpgbin0 -> 363659 bytes
-rw-r--r--res/drawable-nodpi/photo_119_003.jpgbin0 -> 483796 bytes
-rw-r--r--res/drawable-nodpi/photo_119_004.jpgbin0 -> 376106 bytes
-rw-r--r--res/drawable-nodpi/photo_126_001.jpgbin0 -> 466321 bytes
-rw-r--r--res/drawable-nodpi/photo_147_002.jpgbin0 -> 340770 bytes
-rw-r--r--res/drawable-nodpi/photo_175_004.jpgbin0 -> 229233 bytes
-rw-r--r--res/drawable-xhdpi/icon.pngbin0 -> 6817 bytes
-rw-r--r--res/layout/photo.xml22
-rw-r--r--res/mipmap-hdpi/icon.pngbin0 -> 4713 bytes
-rw-r--r--res/mipmap-mdpi/icon.pngbin0 -> 2850 bytes
-rw-r--r--res/values-sw600dp/config.xml26
-rw-r--r--res/values-sw800dp/config.xml26
-rw-r--r--res/values/colors.xml18
-rw-r--r--res/values/config.xml33
-rw-r--r--res/values/dimen.xml18
-rw-r--r--res/values/ids.xml20
-rw-r--r--res/values/strings.xml19
-rw-r--r--src/com/android/dreams/phototable/LocalSource.java193
-rw-r--r--src/com/android/dreams/phototable/PhotoTable.java514
33 files changed, 942 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..40ee416
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,19 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13 \
+ google-common
+
+LOCAL_PACKAGE_NAME := PhotoTable
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..114a8ef
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dreams.phototable"
+ >
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+ <application
+ android:label="@string/app_name"
+ android:icon="@mipmap/icon"
+ android:largeHeap="true">
+ <service android:name="PhotoTable"
+ android:exported="true"
+ android:label="@string/screensaver_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.DREAM" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/res/anim/slideshow_in.xml b/res/anim/slideshow_in.xml
new file mode 100644
index 0000000..225b250
--- /dev/null
+++ b/res/anim/slideshow_in.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="1500" />
diff --git a/res/anim/slideshow_out.xml b/res/anim/slideshow_out.xml
new file mode 100644
index 0000000..5438e17
--- /dev/null
+++ b/res/anim/slideshow_out.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/accelerate_quad"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:duration="1500"
+/>
diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..b57ffa4
--- /dev/null
+++ b/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..d712819
--- /dev/null
+++ b/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..5cd2f30
--- /dev/null
+++ b/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/res/drawable-nodpi/frame.9.png b/res/drawable-nodpi/frame.9.png
new file mode 100644
index 0000000..5262ccf
--- /dev/null
+++ b/res/drawable-nodpi/frame.9.png
Binary files differ
diff --git a/res/drawable-nodpi/photo_039_002.jpg b/res/drawable-nodpi/photo_039_002.jpg
new file mode 100644
index 0000000..f2fa084
--- /dev/null
+++ b/res/drawable-nodpi/photo_039_002.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_044_002.jpg b/res/drawable-nodpi/photo_044_002.jpg
new file mode 100644
index 0000000..a05ea32
--- /dev/null
+++ b/res/drawable-nodpi/photo_044_002.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_059_003.jpg b/res/drawable-nodpi/photo_059_003.jpg
new file mode 100644
index 0000000..6689a31
--- /dev/null
+++ b/res/drawable-nodpi/photo_059_003.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_070_004.jpg b/res/drawable-nodpi/photo_070_004.jpg
new file mode 100644
index 0000000..3821443
--- /dev/null
+++ b/res/drawable-nodpi/photo_070_004.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_072_001.jpg b/res/drawable-nodpi/photo_072_001.jpg
new file mode 100644
index 0000000..6ef496a
--- /dev/null
+++ b/res/drawable-nodpi/photo_072_001.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_077_002.jpg b/res/drawable-nodpi/photo_077_002.jpg
new file mode 100644
index 0000000..c3d9386
--- /dev/null
+++ b/res/drawable-nodpi/photo_077_002.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_098_002.jpg b/res/drawable-nodpi/photo_098_002.jpg
new file mode 100644
index 0000000..3012636
--- /dev/null
+++ b/res/drawable-nodpi/photo_098_002.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_119_003.jpg b/res/drawable-nodpi/photo_119_003.jpg
new file mode 100644
index 0000000..7d468e5
--- /dev/null
+++ b/res/drawable-nodpi/photo_119_003.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_119_004.jpg b/res/drawable-nodpi/photo_119_004.jpg
new file mode 100644
index 0000000..a1dc8cd
--- /dev/null
+++ b/res/drawable-nodpi/photo_119_004.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_126_001.jpg b/res/drawable-nodpi/photo_126_001.jpg
new file mode 100644
index 0000000..072006b
--- /dev/null
+++ b/res/drawable-nodpi/photo_126_001.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_147_002.jpg b/res/drawable-nodpi/photo_147_002.jpg
new file mode 100644
index 0000000..3ee61b2
--- /dev/null
+++ b/res/drawable-nodpi/photo_147_002.jpg
Binary files differ
diff --git a/res/drawable-nodpi/photo_175_004.jpg b/res/drawable-nodpi/photo_175_004.jpg
new file mode 100644
index 0000000..5ff2ad2
--- /dev/null
+++ b/res/drawable-nodpi/photo_175_004.jpg
Binary files differ
diff --git a/res/drawable-xhdpi/icon.png b/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000..5f94c42
--- /dev/null
+++ b/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/res/layout/photo.xml b/res/layout/photo.xml
new file mode 100644
index 0000000..988dbe4
--- /dev/null
+++ b/res/layout/photo.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/photo"
+ android:layout_width="1024dp"
+ android:layout_height="1024dp"
+ android:scaleType="centerInside"
+ />
+
diff --git a/res/mipmap-hdpi/icon.png b/res/mipmap-hdpi/icon.png
new file mode 100644
index 0000000..b57ffa4
--- /dev/null
+++ b/res/mipmap-hdpi/icon.png
Binary files differ
diff --git a/res/mipmap-mdpi/icon.png b/res/mipmap-mdpi/icon.png
new file mode 100644
index 0000000..5cd2f30
--- /dev/null
+++ b/res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..74122db
--- /dev/null
+++ b/res/values-sw600dp/config.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- Number of photos to drop when the screensaver starts.-->
+ <integer name="initial_drop">8</integer>
+
+ <!-- Maximum number of photos to leave on the table.-->
+ <integer name="table_capacity">8</integer>
+
+ <!-- Parts per million ratio between image size and screen size. -->
+ <integer name="image_ratio">500000</integer>
+</resources>
+
diff --git a/res/values-sw800dp/config.xml b/res/values-sw800dp/config.xml
new file mode 100644
index 0000000..5288f98
--- /dev/null
+++ b/res/values-sw800dp/config.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- Number of photos to drop when the screensaver starts.-->
+ <integer name="initial_drop">3</integer>
+
+ <!-- Maximum number of photos to leave on the table.-->
+ <integer name="table_capacity">3</integer>
+
+ <!-- Parts per million ratio between image size and screen size. -->
+ <integer name="image_ratio">500000</integer>
+</resources>
+
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..9d5864e
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <color name="tabletop">#ff444444</color>
+</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..f1c9fcc
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- Milliseconds between drops, needs to be tuned to slide_duration
+ to control the number of photos on the table at any given time. -->
+ <integer name="drop_period">30000</integer>
+
+ <!-- Number of photos to drop when the screensaver starts.-->
+ <integer name="initial_drop">10</integer>
+
+ <!-- Maximum number of photos to leave on the table.-->
+ <integer name="table_capacity">10</integer>
+
+ <!-- Parts per million ratio between image size and screen size. -->
+ <integer name="image_ratio">1000000</integer>
+
+ <!-- Maximum number of image paths to load before shuffling. -->
+ <integer name="image_queue_size">1000</integer>
+</resources>
+
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
new file mode 100644
index 0000000..7e070df
--- /dev/null
+++ b/res/values/dimen.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <dimen name="photo_inset">4px</dimen>
+</resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
new file mode 100644
index 0000000..3f65a0c
--- /dev/null
+++ b/res/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <item type="id" name="photo_height" />
+ <item type="id" name="photo_width" />
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..94e2f9c
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <string name="app_name">Photo Table Screensaver</string>
+ <string name="screensaver_name">Photo Table</string>
+</resources>
diff --git a/src/com/android/dreams/phototable/LocalSource.java b/src/com/android/dreams/phototable/LocalSource.java
new file mode 100644
index 0000000..1414567
--- /dev/null
+++ b/src/com/android/dreams/phototable/LocalSource.java
@@ -0,0 +1,193 @@
+/*
+ * 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.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.BufferedInputStream;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Random;
+
+/**
+ * Picks a random image from the local store.
+ */
+public class LocalSource {
+ private static final String TAG = "PhotoTable.LocalSource";
+ static final boolean DEBUG = false;
+
+ public static class ImageData {
+ public String path;
+ public int orientation;
+ }
+
+ private final ContentResolver mResolver;
+ private final Context mContext;
+ private final LinkedList<ImageData> mImageQueue;
+ private final float mImageRatio;
+ private final int mMaxQueueSize;
+ private final Random mRNG;
+ private int mNextPosition;
+
+ public LocalSource(Context context) {
+ mContext = context;
+ mResolver = mContext.getContentResolver();
+ mNextPosition = -1;
+ mImageQueue = new LinkedList<ImageData>();
+ mImageRatio = context.getResources().getInteger(R.integer.image_ratio) / 1000000f;
+ mMaxQueueSize = context.getResources().getInteger(R.integer.image_queue_size);
+ mRNG = new Random();
+ fillQueue();
+ }
+
+ private void fillQueue() {
+ log("filling queue");
+ String[] projection = {MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION};
+ Cursor cursor = mResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ projection, null, null, null);
+ if (cursor != null) {
+ if (cursor.getCount() > mMaxQueueSize && mNextPosition == -1) {
+ mNextPosition = mRNG.nextInt() % (cursor.getCount() - mMaxQueueSize);
+ }
+ if (mNextPosition == -1) {
+ mNextPosition = 0;
+ }
+ cursor.moveToPosition(mNextPosition);
+
+ int dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
+ int orientationIndex = cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION);
+ if (dataIndex < 0) {
+ log("can't find the DATA column!");
+ } else {
+ while (mImageQueue.size() < mMaxQueueSize && !cursor.isAfterLast()) {
+ ImageData data = new ImageData();
+ data.path = cursor.getString(dataIndex);
+ data.orientation = cursor.getInt(orientationIndex);
+ mImageQueue.offer(data);
+ if (cursor.moveToNext()) {
+ mNextPosition++;
+ }
+ }
+ if (cursor.isAfterLast()) {
+ mNextPosition = 0;
+ }
+ }
+ cursor.close();
+ }
+ Collections.shuffle(mImageQueue);
+ log("queue contains: " + mImageQueue.size() + " items.");
+ }
+
+ public Bitmap next(BitmapFactory.Options options, int longSide, int shortSide) {
+ log("decoding a local resource to " + longSide + ", " + shortSide);
+ Bitmap image = null;
+
+ if (mImageQueue.isEmpty()) {
+ fillQueue();
+ }
+
+ if (!mImageQueue.isEmpty()) {
+ ImageData data = mImageQueue.poll();
+
+ FileInputStream fis = null;
+ try {
+ log("decoding:" + data.path);
+ fis = new FileInputStream(data.path);
+ options.inJustDecodeBounds = true;
+ options.inSampleSize = 1;
+ BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
+ int rawLongSide = Math.max(options.outWidth, options.outHeight);
+ int rawShortSide = Math.max(options.outWidth, options.outHeight);
+ log("I see bounds of " + rawLongSide + ", " + rawShortSide);
+
+ float ratio = Math.min((float) longSide / (float) rawLongSide,
+ (float) shortSide / (float) rawShortSide);
+ while (ratio < 0.5) {
+ options.inSampleSize *= 2;
+ ratio *= 2;
+ }
+
+ log("decoding with inSampleSize " + options.inSampleSize);
+ options.inJustDecodeBounds = false;
+ image = BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
+ rawLongSide = Math.max(options.outWidth, options.outHeight);
+ rawShortSide = Math.max(options.outWidth, options.outHeight);
+ ratio = Math.min((float) longSide / (float) rawLongSide,
+ (float) shortSide / (float) rawShortSide);
+
+ if (ratio < 1.0f) {
+ log("still too big, scaling down by " + ratio);
+ options.outWidth = (int) (ratio * options.outWidth);
+ options.outHeight = (int) (ratio * options.outHeight);
+ image = Bitmap.createScaledBitmap(image,
+ options.outWidth, options.outHeight,
+ true);
+ }
+
+ if (data.orientation != 0) {
+ log("rotated by " + data.orientation + ": fixing");
+ if (data.orientation == 90 || data.orientation == 270) {
+ int tmp = options.outWidth;
+ options.outWidth = options.outHeight;
+ options.outHeight = tmp;
+ }
+ Matrix matrix = new Matrix();
+ matrix.setRotate(- data.orientation,
+ (float) image.getWidth() / 2,
+ (float) image.getHeight() / 2);
+ image = Bitmap.createBitmap(image, 0, 0,
+ options.outWidth, options.outHeight,
+ matrix, true);
+ }
+
+ log("returning bitmap sized to " + image.getWidth() + ", " + image.getHeight());
+ } catch (Exception ex) {
+ log(ex.toString());
+ return null;
+ } finally {
+ try {
+ if (fis != null) {
+ fis.close();
+ }
+ } catch (Throwable t) {
+ log("close fail: " + t.toString());
+ }
+ }
+ } else {
+ log("device has no local images.");
+ }
+
+ return image;
+ }
+
+ public void setSeed(long seed) {
+ mRNG.setSeed(seed);
+ }
+
+ private void log(String message) {
+ if (DEBUG) {
+ Log.i(TAG, message);
+ }
+ }
+}
diff --git a/src/com/android/dreams/phototable/PhotoTable.java b/src/com/android/dreams/phototable/PhotoTable.java
new file mode 100644
index 0000000..a8b36ea
--- /dev/null
+++ b/src/com/android/dreams/phototable/PhotoTable.java
@@ -0,0 +1,514 @@
+/*
+ * 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.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+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.os.PowerManager;
+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.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Random;
+
+/**
+ * Example interactive screen saver.
+ */
+public class PhotoTable extends Dream {
+ private static final String TAG = "PhotoTable";
+ private static final boolean DEBUG = false;
+ private static final int[] PHOTOS = {R.drawable.photo_044_002,
+ R.drawable.photo_039_002,
+ R.drawable.photo_059_003,
+ R.drawable.photo_070_004,
+ R.drawable.photo_072_001,
+ R.drawable.photo_077_002,
+ R.drawable.photo_098_002,
+ R.drawable.photo_119_003,
+ R.drawable.photo_119_004,
+ R.drawable.photo_126_001,
+ R.drawable.photo_147_002,
+ R.drawable.photo_175_004
+ };
+
+ private Table mTable;
+
+ public static class Table extends FrameLayout {
+ class Launcher implements Runnable {
+ private final Table mTable;
+ public Launcher(Table table) {
+ mTable = table;
+ }
+
+ @Override
+ public void run() {
+ mTable.launch();
+ }
+ }
+
+ private static final long MAX_SELECTION_TIME = 10000L;
+ private static Random sRNG = new Random();
+
+ private final Launcher mLauncher;
+ private boolean mStarted;
+ private LinkedList<View> mOnTable;
+ private Dream mDream;
+ private int mDropPeriod;
+ private float mImageRatio;
+ private int mTableCapacity;
+ private int mInset;
+ private BitmapFactory.Options mOptions;
+ private int mLongSide;
+ private int mShortSide;
+ private int mWidth;
+ private int mHeight;
+ private View mSelected;
+ private long mSelectedTime;
+ private LocalSource mLocalSource;
+ private Resources mResources;
+ private PointF[] mDropZone;
+
+ public Table(Dream dream, AttributeSet as) {
+ super(dream, as);
+ mDream = dream;
+ mResources = getResources();
+ setBackgroundColor(mResources.getColor(R.color.tabletop));
+ mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset);
+ mDropPeriod = mResources.getInteger(R.integer.drop_period);
+ mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f;
+ mTableCapacity = mResources.getInteger(R.integer.table_capacity);
+ mOnTable = new LinkedList<View>();
+ mOptions = new BitmapFactory.Options();
+ mOptions.inTempStorage = new byte[32768];
+ mLocalSource = new LocalSource(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);
+ mSelected = selected;
+ mSelectedTime = System.currentTimeMillis();
+ }
+
+ 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 {
+ 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;
+
+ if (mDropZone == null) {
+ mDropZone = new PointF[4];
+ mDropZone[0] = new PointF();
+ mDropZone[1] = new PointF();
+ mDropZone[2] = new PointF();
+ mDropZone[3] = new PointF();
+ }
+ mDropZone[0].x = 0f;
+ mDropZone[0].y = 0.75f * mHeight;
+ mDropZone[1].x = 0f;
+ mDropZone[1].y = 0f;
+ mDropZone[2].x = 0f;
+ mDropZone[2].y = 0f;
+ mDropZone[3].x = 0.75f * mWidth;
+ mDropZone[3].y = 0f;
+
+ mLongSide = Math.max(mWidth, mHeight);
+ mShortSide = Math.min(mWidth, mHeight);
+
+ start();
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return true;
+ }
+
+ @SuppressWarnings("deprecation")
+ private void launch() {
+ scheduleNext();
+
+ log("launching");
+ setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
+ if (hasSelection() &&
+ (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) {
+ dropOnTable(getSelected());
+ clearSelection();
+ } else {
+ log("inflate it");
+ AsyncTask<Void, Void, View> task = new AsyncTask<Void, Void, View>() {
+ @Override
+ public View doInBackground(Void... unused) {
+ log("load a new photo");
+ LayoutInflater inflater = (LayoutInflater) 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 = null;
+ decodedPhoto = mLocalSource.next(mOptions, mLongSide, mShortSide);
+ if (decodedPhoto == null) {
+ decodedPhoto = nextStockPhoto(mOptions, mLongSide, mShortSide);
+ }
+ int photoWidth = mOptions.outWidth;
+ int photoHeight = mOptions.outHeight;
+ if (mOptions.outWidth <= 0 || mOptions.outHeight <= 0) {
+ photo = null;
+ } else {
+ layers[0] = new BitmapDrawable(mResources, decodedPhoto);
+ layers[1] = mResources.getDrawable(R.drawable.frame);
+ LayerDrawable layerList = new LayerDrawable(layers);
+ layerList.setLayerInset(0, mInset, mInset, mInset, mInset);
+ image.setImageDrawable(layerList);
+
+ photo.setTag(R.id.photo_width, new Integer(photoWidth));
+ photo.setTag(R.id.photo_height, new Integer(photoHeight));
+ }
+
+ return photo;
+ }
+
+ @Override
+ public void onPostExecute(View photo) {
+ if (photo != null) {
+ addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ if (hasSelection()) {
+ bringChildToFront(getSelected());
+ }
+ int width = ((Integer) photo.getTag(R.id.photo_width)).intValue();
+ int height = ((Integer) photo.getTag(R.id.photo_height)).intValue();
+
+ log("drop it");
+ throwOnTable(photo);
+ }
+ }
+ };
+ task.execute();
+ }
+ }
+
+ private Bitmap nextStockPhoto(BitmapFactory.Options options, int longSide, int shortSide) {
+ log("decoding a local resource to " + longSide + ", " + shortSide);
+ int photo = PHOTOS[Math.abs(sRNG.nextInt() % PHOTOS.length)];
+
+ options.inJustDecodeBounds = true;
+ options.inSampleSize = 1;
+ BitmapFactory.decodeResource(mResources, photo, options);
+ int rawLongSide = Math.max(options.outWidth, options.outHeight);
+ int rawShortSide = Math.max(options.outWidth, options.outHeight);
+ log("I see bounds of " + rawLongSide + ", " + rawShortSide);
+ float ratio = Math.min((float) longSide / (float) rawLongSide,
+ (float) shortSide / (float) rawShortSide);
+ while (ratio < 0.5) {
+ options.inSampleSize *= 2;
+ ratio *= 2;
+ }
+ log("decoding with inSampleSize " + options.inSampleSize);
+ options.inJustDecodeBounds = false;
+ Bitmap bitmap = BitmapFactory.decodeResource(mResources, photo, options);
+ rawLongSide = Math.max(options.outWidth, options.outHeight);
+ rawShortSide = Math.max(options.outWidth, options.outHeight);
+ ratio = Math.min((float) longSide / (float) rawLongSide,
+ (float) shortSide / (float) rawShortSide);
+
+ if (ratio < 1.0f) {
+ log("still too big, scaling down by " + ratio);
+ int photoWidth = (int) (ratio * options.outWidth);
+ int photoHeight = (int) (ratio * options.outHeight);
+ bitmap = Bitmap.createScaledBitmap(bitmap, photoWidth, photoHeight, true);
+ }
+
+ log("returning bitmap sized to " + bitmap.getWidth() + ", " + bitmap.getHeight());
+ return bitmap;
+ }
+
+ private void fadeAway(final View photo) {
+ // fade out of view
+ photo.animate().cancel();
+ photo.animate()
+ .withLayer()
+ .alpha(0f)
+ .setDuration(1000)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ removeView(photo);
+ recycle(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(-width);
+ photo.setY(-height);
+ dropOnTable(photo);
+ }
+
+ private void dropOnTable(final View photo) {
+ float angle = randfrange(-60, 60f);
+ 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 -= width / 2f;
+ y -= height / 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(0.5f)
+ .scaleY(0.5f)
+ .rotation(angle)
+ .x(x)
+ .y(y)
+ .setDuration(duration)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ while (mOnTable.size() > mTableCapacity) {
+ fadeAway(mOnTable.poll());
+ }
+ }
+ });
+
+ photo.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View target) {
+ if (hasSelection()) {
+ dropOnTable(getSelected());
+ clearSelection();
+ }
+ target.animate().cancel();
+ bringChildToFront(target);
+ setSelection(target);
+ pickUp(getSelected());
+ }
+ });
+ }
+
+ 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);
+
+ log("animate it");
+ // toss onto table
+ photo.animate()
+ .withLayer()
+ .rotation(0f)
+ .scaleX(scale)
+ .scaleY(scale)
+ .x(x)
+ .y(y)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator())
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ log("endtimes: " + photo.getX());
+ }
+ });
+
+ photo.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View target) {
+ if (getSelected() == photo) {
+ dropOnTable(photo);
+ clearSelection();
+ }
+ }
+ });
+ }
+
+ private static void log(String message) {
+ if (DEBUG) {
+ Log.i(TAG, message);
+ }
+ }
+
+ 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 recycleAll() {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public void onPreExecute() {
+ for (View photo: mOnTable) {
+ removeView(photo);
+ }
+ }
+
+ @Override
+ public Void doInBackground(Void... unused) {
+ while (!mOnTable.isEmpty()) {
+ recycle(mOnTable.poll());
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ public void start() {
+ if (!mStarted) {
+ log("kick it");
+ mStarted = true;
+ for (int i = 0; i < mResources.getInteger(R.integer.initial_drop); i++) {
+ launch();
+ }
+ }
+ }
+
+ public void scheduleNext() {
+ removeCallbacks(mLauncher);
+ postDelayed(mLauncher, mDropPeriod);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setInteractive(true);
+ mTable = new Table(this, null);
+ setContentView(mTable);
+ lightsOut();
+ }
+
+ @Override
+ public void onDestroy() {
+ mTable.recycleAll();
+ super.onDestroy();
+ }
+}