Michel Roux 9 years ago
parent 9e6e4b8311
commit 6fb13ea8da

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.derpfish.pinkielive"
android:versionName="1.2.2"
android:versionCode="5">
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="19" />
<uses-feature android:name="android.software.live_wallpaper" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application android:icon="@drawable/ic_launcher"
android:label="Pinkie Pie Live">
<service android:name=".PinkiePieLiveWallpaper"
android:label="Pinkie Pie Live"
android:icon="@drawable/ic_launcher"
android:permission="android.permission.BIND_WALLPAPER">
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_SCANNER_FINISHED"/>
<data android:scheme="file" />
</intent-filter>
<meta-data android:name="android.service.wallpaper"
android:resource="@xml/livewallpaper" />
</service>
<activity android:label="Settings"
android:name=".preference.PinkiePieLiveWallpaperSettings"
android:theme="@android:style/Theme.Light.WallpaperSettings"
android:exported="true"
android:icon="@drawable/ic_launcher">
</activity>
<activity android:label="Advanced Settings"
android:name=".preference.PinkiePieLiveWallpaperSettingsAdvanced"
android:theme="@android:style/Theme.Light">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=Google Inc.:Google APIs:19

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dip"
android:layout_marginLeft="15dip"
android:layout_marginRight="6dip"
android:layout_marginTop="6dip"
android:layout_weight="1"
android:baselineAligned="false"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingRight="?android:attr/scrollbarSize" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/title"
android:layout_below="@id/title"
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="credits_string">
<![CDATA[
Pinkie Pie Live application written by melknin: <a href="mailto:melknin@gmail.com">melknin@gmail.com</a>
<br/><br/>
Pinkie Pie Animations by melknin, adapted from screencaps of S01E26
<br/><br/>
Pinkie Pie Icon - <a href="http://cptofthefriendship.deviantart.com/art/Pinkie-Pie-4th-wall-279458561">CptOfTheFriendship</a>
<br/><br/>
Rarity Animation Image - <a href="http://misteraibo.deviantart.com/art/Travelling-Filly-Rarity-Vector-255586607">MisterAibo</a>
<br/><br/>
Default Background - Pulled from The TV DB (original source unknown; if you know, tell me so I can give proper credit!)
]]>
</string>
</resources>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.derpfish.pinkielive.preference.PinkiePieLiveWallpaperSettings"
android:thumbnail="@drawable/ic_launcher"/>

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="Settings"
android:key="livewallpaper_settings">
<CheckBoxPreference android:key="livewallpaper_defaultbg"
android:defaultValue="true"
android:summary="Use Default Background"
android:title="Use Default Background"
android:summaryOn="Using Default Default"
android:summaryOff="Using Selected Image"/>
<Preference android:key="livewallpaper_image"
android:title="Select Background Image"
android:summary="Select Background" />
<com.derpfish.pinkielive.preference.PonyListPreference
android:key="livewallpaper_pony"
android:title="Select Pony"
android:summary="Pick the pony to grace your screen"
android:defaultValue="pinkie" />
<com.derpfish.pinkielive.preference.PonyDownloadListPreference
android:key="livewallpaper_download"
android:title="Download More Ponies"
android:summary="Fetch animations from the project page" />
<PreferenceScreen android:title="Advanced Settings"
android:summary="Tweaks and Workarounds"
android:theme="@android:style/Theme.Light.WallpaperSettings">
<intent android:action="android.intent.action.VIEW"
android:targetPackage="com.derpfish.pinkielive"
android:targetClass="com.derpfish.pinkielive.preference.PinkiePieLiveWallpaperSettingsAdvanced"/>
</PreferenceScreen>
<com.derpfish.pinkielive.preference.CreditsPreference
android:title="Credits" />
</PreferenceScreen>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="Advanced Settings"
android:key="livewallpaper_settings_advanced">
<EditTextPreference android:key="livewallpaper_framerate"
android:title="Framerate"
android:summary="Frames per second for pony animations"
android:numeric="integer"
android:defaultValue="60" />
<CheckBoxPreference android:key="livewallpaper_enableparallax"
android:defaultValue="true"
android:title="Enable Parallax Scrolling"
android:summaryOn="Parallax Scrolling Enabled"
android:summaryOff="Parallax Scrolling Disabled" />
</PreferenceScreen>

@ -0,0 +1,39 @@
package com.derpfish.pinkielive;
import java.io.InputStream;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class BitmapLoader
{
public static Bitmap decodeSampledBitmapFromInputStream(InputStream istream, int sampleSize)
{
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
return BitmapFactory.decodeStream(istream, null, options);
}
public static int getSampleSizeFromInputStream(InputStream istream, int reqWidth, int reqHeight)
{
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(istream, null, options);
// Calculate inSampleSize
return calculateInSampleSize(options, reqWidth, reqHeight);
}
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
return Math.max(1, Math.max(
Math.round((float) height / (float) reqHeight),
Math.round((float) width / (float) reqWidth)));
}
}

@ -0,0 +1,425 @@
package com.derpfish.pinkielive;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import com.derpfish.pinkielive.animation.PinkieAnimation;
import com.derpfish.pinkielive.animation.PonyAnimation;
import com.derpfish.pinkielive.download.PonyAnimationContainer;
import com.derpfish.pinkielive.download.PonyDownloader;
import android.app.WallpaperManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
public class PinkiePieLiveWallpaper extends WallpaperService
{
public static final String SHARED_PREFS_NAME = "livewallpapersettings";
// Round-trip time for a jump to complete in ms.
public static final double TIME_FOR_JUMP = 1500.0;
private Bitmap defaultBg = null;
// Settings
private Bitmap selectedBg = null;
private boolean useDefaultBg = true;
private long targetFramerate = 60L;
private boolean enableParallax = true;
private PonyAnimation selectedPony;
@Override
public void onCreate()
{
super.onCreate();
}
@Override
public void onDestroy()
{
if (defaultBg != null)
{
defaultBg.recycle();
defaultBg = null;
}
if (selectedBg != null)
{
selectedBg.recycle();
selectedBg = null;
}
if (selectedPony != null)
{
selectedPony.onDestroy();
selectedPony = null;
}
super.onDestroy();
}
@Override
public Engine onCreateEngine()
{
return new PonyEngine();
}
class PonyEngine extends Engine implements SharedPreferences.OnSharedPreferenceChangeListener
{
private long lastUpdate;
private int surfaceWidth;
private int surfaceHeight;
private float offsetX;
private float offsetY;
private final Handler mHandler = new Handler();
private final Paint mPaint = new Paint();
private final Runnable mDrawPattern = new Runnable()
{
public void run()
{
drawFrame();
}
};
private boolean mVisible;
private final SharedPreferences mPreferences;
private final BroadcastReceiver broadcastReceiver;
private PonyEngine()
{
final Paint paint = mPaint;
paint.setColor(0xffffffff);
paint.setAntiAlias(true);
paint.setStrokeWidth(2);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStyle(Paint.Style.STROKE);
mPreferences = PinkiePieLiveWallpaper.this.getSharedPreferences(SHARED_PREFS_NAME, 0);
mPreferences.registerOnSharedPreferenceChangeListener(this);
/*
* If the media scanner finishes a scan, reload the preferences
* since this means a previously unavailable background is now
* available.
*/
final IntentFilter iFilter = new IntentFilter();
iFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
iFilter.addDataScheme("file");
broadcastReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
final String action = intent.getAction();
if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED))
{
onSharedPreferenceChanged(mPreferences, null);
}
}
};
registerReceiver(broadcastReceiver, iFilter);
// Load saved preferences
onSharedPreferenceChanged(mPreferences, null);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key)
{
useDefaultBg = prefs.getBoolean("livewallpaper_defaultbg", true);
if (useDefaultBg)
{
if (selectedBg != null)
{
selectedBg.recycle();
selectedBg = null;
}
try
{
final WallpaperManager wmMan = WallpaperManager.getInstance(getApplicationContext());
final AssetManager assetManager = getAssets();
InputStream istr = assetManager.open("defaultbg.jpg");
final int sampleSize = BitmapLoader.getSampleSizeFromInputStream(istr,
wmMan.getDesiredMinimumWidth(), wmMan.getDesiredMinimumHeight());
istr.close();
istr = assetManager.open("defaultbg.jpg");
defaultBg = BitmapLoader.decodeSampledBitmapFromInputStream(istr, sampleSize);
istr.close();
}
catch (IOException e)
{
throw new IllegalStateException("Could not find background image");
}
}
else
{
if (defaultBg != null)
{
defaultBg.recycle();
defaultBg = null;
}
final String imageUriStr = prefs.getString("livewallpaper_image", null);
if (imageUriStr != null)
{
if (selectedBg != null)
{
selectedBg.recycle();
}
final Uri bgImage = Uri.parse(imageUriStr);
try
{
final ContentResolver contentResolver = getContentResolver();
final WallpaperManager wmMan = WallpaperManager.getInstance(getApplicationContext());
InputStream istr = contentResolver.openInputStream(bgImage);
final int sampleSize = BitmapLoader.getSampleSizeFromInputStream(istr,
wmMan.getDesiredMinimumWidth(), wmMan.getDesiredMinimumHeight());
istr.close();
istr = contentResolver.openInputStream(bgImage);
selectedBg = BitmapLoader.decodeSampledBitmapFromInputStream(istr, sampleSize);
istr.close();
}
catch (IOException e)
{
selectedBg = null;
Log.w("PinkieLive", e);
}
}
}
// Framerate
final String frameratePref = prefs.getString("livewallpaper_framerate", "60");
try
{
targetFramerate = Long.parseLong(frameratePref);
}
catch (NumberFormatException e)
{
Log.e("PinkieLive", e.getMessage());
}
// Parallax
enableParallax = prefs.getBoolean("livewallpaper_enableparallax", true);
if (!enableParallax)
{
offsetX = offsetY = 0.0f;
}
// Change selected pony
if (selectedPony != null)
{
selectedPony.onDestroy();
}
// Refresh pony animations; preference change could have updated them
final Map<String, PonyAnimation> ponyAnimations = new HashMap<String, PonyAnimation>();
ponyAnimations.put("pinkie", new PinkieAnimation(getAssets()));
try
{
for (final PonyAnimationContainer container : PonyDownloader.getPonyAnimations(
getFilesDir(), getCacheDir(), true))
{
ponyAnimations.put(container.getId(), container.getPonyAnimation());
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
// Set new pony animation
final String selectedPonyId = prefs.getString("livewallpaper_pony", "pinkie");
selectedPony = ponyAnimations.containsKey(selectedPonyId) ? ponyAnimations.get(selectedPonyId) : ponyAnimations.get("pinkie");
selectedPony.onCreate();
drawFrame();
}
@Override
public void onCreate(SurfaceHolder surfaceHolder)
{
super.onCreate(surfaceHolder);
setTouchEventsEnabled(true);
}
@Override
public void onDestroy()
{
super.onDestroy();
mHandler.removeCallbacks(mDrawPattern);
unregisterReceiver(broadcastReceiver);
}
@Override
public void onVisibilityChanged(boolean visible)
{
mVisible = visible;
if (visible)
{
drawFrame();
}
else
{
mHandler.removeCallbacks(mDrawPattern);
}
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
super.onSurfaceChanged(holder, format, width, height);
surfaceWidth = width;
surfaceHeight = height;
drawFrame();
}
@Override
public void onSurfaceCreated(SurfaceHolder holder)
{
super.onSurfaceCreated(holder);
}
@Override
public void onSurfaceDestroyed(SurfaceHolder holder)
{
super.onSurfaceDestroyed(holder);
mVisible = false;
mHandler.removeCallbacks(mDrawPattern);
}
@Override
public void onOffsetsChanged(float xOffset, float yOffset, float xStep, float yStep, int xPixels, int yPixels)
{
if (enableParallax)
{
offsetX = xPixels;
offsetY = yPixels;
drawFrame();
}
}
/*
* Store the position of the touch event so we can use it for drawing
* later
*/
@Override
public void onTouchEvent(MotionEvent event)
{
if (event.getAction() == MotionEvent.ACTION_UP)
{
// If the length of time pressed was less than 0.5 seconds,
// trigger a new drawing
if (event.getEventTime() - event.getDownTime() < 500)
{
if (selectedPony.isComplete())
{
selectedPony.initialize(surfaceWidth, surfaceHeight, event.getX(), event.getY());
lastUpdate = SystemClock.elapsedRealtime();
drawFrame();
}
}
}
super.onTouchEvent(event);
}
/*
* Draw one frame of the animation. This method gets called repeatedly
* by posting a delayed Runnable. You can do any drawing you want in
* here. This example draws a wireframe cube.
*/
private void drawFrame()
{
final SurfaceHolder holder = getSurfaceHolder();
Canvas c = null;
try
{
c = holder.lockCanvas();
if (c != null)
{
// Blank canvas
final Paint paintBlack = new Paint();
paintBlack.setColor(0xff000000);
c.drawRect(0.0f, 0.0f, surfaceWidth, surfaceHeight, paintBlack);
// draw something
if (useDefaultBg || selectedBg != null)
{
final Bitmap actualBg = selectedBg != null ? selectedBg : defaultBg;
if (enableParallax)
{
final WallpaperManager wmMan = WallpaperManager.getInstance(getApplicationContext());
final int minWidth = wmMan.getDesiredMinimumWidth();
final int minHeight = wmMan.getDesiredMinimumHeight();
final float bgScale = Math.min(((float)actualBg.getWidth()) / ((float)minWidth), ((float)actualBg.getHeight()) / ((float)minHeight));
final int centeringOffsetX = (int)((float)actualBg.getWidth() - bgScale*minWidth)/2;
final int centeringOffsetY = (int)((float)actualBg.getHeight() - bgScale*minHeight)/2;
c.drawBitmap(actualBg,
new Rect(centeringOffsetX - (int)(offsetX*bgScale),
centeringOffsetY - (int)(offsetY*bgScale),
centeringOffsetX + (int)((-offsetX + surfaceWidth)*bgScale),
centeringOffsetY + (int)((-offsetY + surfaceHeight)*bgScale)),
new Rect(0, 0, surfaceWidth, surfaceHeight), mPaint);
}
else
{
final float bgScale = Math.min(((float)actualBg.getWidth()) / ((float)surfaceWidth), ((float)actualBg.getHeight()) / ((float)surfaceHeight));
final int centeringOffsetX = (int)((float)actualBg.getWidth() - bgScale*surfaceWidth)/2;
final int centeringOffsetY = (int)((float)actualBg.getHeight() - bgScale*surfaceHeight)/2;
c.drawBitmap(actualBg,
new Rect(centeringOffsetX, centeringOffsetY,
centeringOffsetX + (int)(surfaceWidth*bgScale),
centeringOffsetY + (int)(surfaceHeight*bgScale)),
new Rect(0, 0, surfaceWidth, surfaceHeight), mPaint);
}
}
// Decide new position and velocity.
if (!selectedPony.isComplete())
{
final long now = SystemClock.elapsedRealtime();
long elapsedTime = now - lastUpdate;
lastUpdate = now;
selectedPony.drawAnimation(c, elapsedTime);
}
}
}
finally
{
if (c != null)
holder.unlockCanvasAndPost(c);
}
mHandler.removeCallbacks(mDrawPattern);
// Only queue another frame if we're still animating pinkie
if (mVisible && !selectedPony.isComplete())
{
mHandler.postDelayed(mDrawPattern, 1000 / targetFramerate);
}
}
}
}

@ -0,0 +1,212 @@
package com.derpfish.pinkielive.animation;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
public class PinkieAnimation implements PonyAnimation
{
private float surfaceWidth;
private float surfaceHeight;
private float pinkieX;
private double pinkieVelocityX;
private float pinkieRotationAngle;
private float pinkieTargetHeight;
private boolean flipAnimation;
private boolean completed = true;
private long accumulatedTime;
private Bitmap[] bmAnimation = null;
private int lastAnim = -1;
private final Paint mPaint = new Paint();
private final AssetManager assetManager;
private static final int NUM_ANIMATIONS = 7;
private static final long FRAME_DELAY = 50;
private Thread loaderThread;
private final LoadAnimationRunnable loadAnimationRunnable = new LoadAnimationRunnable();
public PinkieAnimation(final AssetManager assetManager)
{
this.assetManager = assetManager;
}
@Override
public void initialize(int surfaceWidth, int surfaceHeight, float tapX, float tapY)
{
this.surfaceWidth = surfaceWidth;
this.surfaceHeight = surfaceHeight;
// Wait for animation to be fully loaded
waitForLoader();
completed = false;
accumulatedTime = 0L;
// Target getting 2/3 of the way up the image to this position
final int PINKIE_WIDTH = (int) Math.min(surfaceWidth * 0.75, surfaceHeight * 0.75);
final float scale = (float) PINKIE_WIDTH / (float) bmAnimation[0].getWidth();
pinkieTargetHeight = Math.min(surfaceHeight / 2, tapY) - scale * (bmAnimation[0].getHeight() / 3.0f);
final double pinkieVelocityY = 4.0 * (pinkieTargetHeight - surfaceHeight) / ((double) (bmAnimation.length * FRAME_DELAY));
pinkieX = (surfaceWidth - tapX);
pinkieVelocityX = 2.0 * (surfaceWidth / 2.0f - pinkieX) / ((double) (bmAnimation.length * FRAME_DELAY));
flipAnimation = (pinkieVelocityX < 0);
pinkieRotationAngle = (float) Math.toDegrees(Math.atan2(pinkieVelocityX, -pinkieVelocityY));
}
@Override
public void drawAnimation(final Canvas canvas, final long elapsedTime)
{
// Decide new position and velocity.
if (!completed)
{
accumulatedTime += elapsedTime;
final int currentFrame = (int) (accumulatedTime / FRAME_DELAY);
final double pinkieY = surfaceHeight + (pinkieTargetHeight - surfaceHeight) * (1 - Math.pow((2.0 * accumulatedTime) / ((double) (bmAnimation.length * FRAME_DELAY)) - 1.0, 4.0));
pinkieX = (float) (elapsedTime * pinkieVelocityX + pinkieX);
if (currentFrame < bmAnimation.length && pinkieY <= surfaceHeight)
{
final Bitmap bitmap = bmAnimation[currentFrame];
final int PINKIE_WIDTH = (int) Math.min(surfaceWidth * 0.75, surfaceHeight * 0.75);
final float scale = (float) PINKIE_WIDTH / (float) bitmap.getWidth();
final Matrix matrix = new Matrix();
if (flipAnimation)
{
matrix.postScale(-1.0f, 1.0f);
matrix.postTranslate(bitmap.getWidth(), 0.0f);
}
matrix.postRotate(pinkieRotationAngle, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
matrix.postScale(scale, scale);
matrix.postTranslate(pinkieX - PINKIE_WIDTH / 2, (float) pinkieY);
canvas.drawBitmap(bitmap, matrix, mPaint);
}
else
{
completed = true;
loadNextAnimation();
}
}
}
private void loadNextAnimation()
{
waitForLoader();
loaderThread = new Thread(loadAnimationRunnable);
loaderThread.start();
}
private void waitForLoader()
{
synchronized (this)
{
if (loaderThread == null)
{
return;
}
try
{
loaderThread.join();
}
catch (InterruptedException e)
{
throw new IllegalStateException("Unhandled interruption: " + e);
}
loaderThread = null;
}
}
private class LoadAnimationRunnable implements Runnable
{
@Override
public void run()
{
lastAnim = (lastAnim + 1) % NUM_ANIMATIONS;
if (bmAnimation != null)
{
for (int i = 0; i < bmAnimation.length; i++)
{
bmAnimation[i].recycle();
}
bmAnimation = null;
}
try
{
final InputStream istr = assetManager.open("jump" + (lastAnim + 1) + ".zip");
final ZipInputStream zis = new ZipInputStream(istr);
final Map<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
ZipEntry zipEntry = null;
while ((zipEntry = zis.getNextEntry()) != null)
{
bitmaps.put(zipEntry.getName(), BitmapFactory.decodeStream(zis));
}
zis.close();
istr.close();
final List<String> names = new ArrayList<String>(bitmaps.keySet());
Collections.sort(names);
bmAnimation = new Bitmap[bitmaps.size()];
for (int i = 0; i < names.size(); i++)
{
bmAnimation[i] = bitmaps.get(names.get(i));
}
}
catch (IOException e)
{
throw new IllegalStateException("Could not load animation: " + lastAnim);
}
}
}
@Override
public boolean isComplete()
{
return completed;
}
@Override
public void onCreate()
{
completed = true;
loadNextAnimation();
}
@Override
public void onDestroy()
{
if (bmAnimation != null)
{
for (int i = 0; i < bmAnimation.length; i++)
{
bmAnimation[i].recycle();
}
}
}
@Override
public void setResourceDir(File resourceDir)
{
}
}

@ -0,0 +1,21 @@
package com.derpfish.pinkielive.animation;
import java.io.File;
import android.graphics.Canvas;
public interface PonyAnimation
{
public void setResourceDir(final File resourceDir);
public void initialize(int surfaceWidth, int surfaceHeight, float tapX, float tapY);
public void drawAnimation(Canvas canvas, long elapsedTime);
public boolean isComplete();
public void onCreate();
public void onDestroy();
}

@ -0,0 +1,51 @@
package com.derpfish.pinkielive.download;
import com.derpfish.pinkielive.animation.PonyAnimation;
public class PonyAnimationContainer
{
private String id;
private String name;
private Long version;
private PonyAnimation ponyAnimation;
public String getId()
{
return id;
}
public void setId(String id)
{
this.id = id;
}
public String getName()
{
return name;
}
public Long getVersion()
{
return version;
}
public void setVersion(Long version)
{
this.version = version;
}
public void setName(String name)
{
this.name = name;
}
public PonyAnimation getPonyAnimation()
{
return ponyAnimation;
}
public void setPonyAnimation(PonyAnimation ponyAnimation)
{
this.ponyAnimation = ponyAnimation;
}
}

@ -0,0 +1,51 @@
package com.derpfish.pinkielive.download;
public class PonyAnimationListing
{
private String name;
private String url;
private String id;
private Long version;
private String checksum;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getUrl()
{
return url;
}
public void setUrl(String url)
{
this.url = url;
}
public String getId()
{
return id;
}
public void setId(String id)
{
this.id = id;
}
public Long getVersion()
{
return version;
}
public void setVersion(Long version)
{
this.version = version;
}
public String getChecksum()
{
return checksum;
}
public void setChecksum(String checksum)
{
this.checksum = checksum;
}
}

@ -0,0 +1,333 @@
package com.derpfish.pinkielive.download;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import com.derpfish.pinkielive.animation.PonyAnimation;
import com.derpfish.pinkielive.util.Base64;
import com.derpfish.pinkielive.util.IOUtils;
import dalvik.system.DexClassLoader;
public class PonyDownloader
{
private static final String animationsUrl = "http://animations.pinkie-live.googlecode.com/git/animations.xml";
private static final String animationsSigUrl = "http://animations.pinkie-live.googlecode.com/git/animations.sig";
private static final String PUBLIC_KEY_ENCODED =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1aP3qdJO5F64+UQVF1zl" +
"DyyM/bN/jJZgU9guq7RLLO81y1IFpxL0Rs66FMkcSz4ZtdtOT6uRhZ9TvrVhMR69" +
"WpxdH3hrh4JhILd+/ZRxQt2GX4FyLDIPqfpA867ZjS+lrgncN48kC2X3z3ETQ46Q" +
"eCjYrLfKeseGy620dWuIV2yXenr1NHSJ5kwOKvOdddEHwijSNwpDo8C93XGCAHtT" +
"fXcmknBeVPNWC1iL0CshpMWuDIvLEh867J6HvpSzyAss0q62mvRyttifjZO8aiSH" +
"LctTLxMnTrxhL9mw4lmFCzI0UoWmeSOiEOQTXhGxWhVE9gXv0jzizTvX5DG9MtTA" +
"CwIDAQAB";
private static Object lock = new Object();
private static PublicKey publicKey = null;
public static boolean verifyData(final InputStream istream, final String base64sig)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException, InvalidKeySpecException
{
synchronized(lock)
{
if (publicKey == null)
{
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(PUBLIC_KEY_ENCODED, Base64.DEFAULT));
final KeyFactory kf = KeyFactory.getInstance("RSA");
publicKey = kf.generatePublic(keySpec);
}
}
final Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
final byte[] buffer = new byte[4096];
int nread = 0;
while ((nread = istream.read(buffer)) >= 0)
{
signature.update(buffer, 0, nread);
}
istream.close();
return signature.verify(Base64.decode(base64sig, Base64.DEFAULT));
}
public static List<PonyAnimationListing> fetchListings() throws ParserConfigurationException, SAXException, IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException
{
final byte[] xmlBytes = fetchUrl(animationsUrl, 131072);
final byte[] sigBytes = fetchUrl(animationsSigUrl, 4096);
if (!verifyData(new ByteArrayInputStream(xmlBytes), new String(sigBytes)))
{
throw new IllegalStateException("Unable to verify signature of animations.xml");
}
return parseListings(new ByteArrayInputStream(xmlBytes));
}
private static byte[] fetchUrl(final String url, int maxSize) throws MalformedURLException, IOException
{
final URLConnection connection = new URL(url).openConnection();
connection.connect();
final InputStream istream = connection.getInputStream();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
int totalRead = 0;
int nRead = 0;
final byte[] buffer = new byte[4096];
while ((nRead = istream.read(buffer)) >= 0)
{
baos.write(buffer, 0, nRead);
totalRead += nRead;
if (maxSize > 0 && totalRead > maxSize)
{
throw new IllegalStateException("Retrieved animations.xml exceeds maximum allowed size.");
}
}
istream.close();
baos.close();
return baos.toByteArray();
}
private static List<PonyAnimationListing> parseListings(final InputStream istream) throws ParserConfigurationException, SAXException, IOException
{
final List<PonyAnimationListing> animationListings = new ArrayList<PonyAnimationListing>();
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
final DocumentBuilder builder = factory.newDocumentBuilder();
final Document dom = builder.parse(istream);
istream.close();
final Element root = dom.getDocumentElement();
if (!root.getNodeName().equals("animations"))
{
throw new IllegalArgumentException("Malformed XML");
}
for (int i = 0; i < root.getChildNodes().getLength(); i++)
{
final Node animation = root.getChildNodes().item(i);
if (animation.getNodeType() == Node.TEXT_NODE)
{
continue;
}
if (!animation.getNodeName().equals("animation"))
{
throw new IllegalArgumentException("Malformed XML");
}
final PonyAnimationListing animListing = new PonyAnimationListing();
for (int j = 0; j < animation.getChildNodes().getLength(); j++)
{
final Node attr = animation.getChildNodes().item(j);
if (attr.getNodeType() == Node.TEXT_NODE)
{
continue;
}
if (attr.getNodeName().equals("name"))
{
if (animListing.getName() != null)
{
throw new IllegalArgumentException("Malformed XML");
}
animListing.setName(getNodeText(attr));
}
else if (attr.getNodeName().equals("url"))
{
if (animListing.getUrl() != null)
{
throw new IllegalArgumentException("Malformed XML");
}
animListing.setUrl(getNodeText(attr));
}
else if (attr.getNodeName().equals("id"))
{
if (animListing.getId() != null)
{
throw new IllegalArgumentException("Malformed XML");
}
animListing.setId(getNodeText(attr));
}
else if (attr.getNodeName().equals("version"))
{
if (animListing.getVersion() != null)
{
throw new IllegalArgumentException("Malformed XML");
}
animListing.setVersion(Long.parseLong(getNodeText(attr)));
}
else if (attr.getNodeName().equals("checksum"))
{
if (animListing.getChecksum() != null)
{
throw new IllegalArgumentException("Malformed XML");
}
animListing.setChecksum(getNodeText(attr));
}
else
{
throw new IllegalArgumentException("Malformed XML");
}
}
if (animListing.getName() == null || animListing.getUrl() == null
|| animListing.getId() == null || animListing.getVersion() == null
|| animListing.getChecksum() == null)
{
throw new IllegalArgumentException("Malformed XML");
}
animationListings.add(animListing);
}
return animationListings;
}
private static String getNodeText(final Node node)
{
if (node.getChildNodes().getLength() != 1)
{
throw new IllegalArgumentException("Malformed XML");
}
final Node innerNode = node.getChildNodes().item(0);
if (innerNode.getNodeType() != Node.TEXT_NODE)
{
throw new IllegalArgumentException("Malformed XML");
}
return innerNode.getNodeValue();
}
public static void fetchPony(final File dataDir, final File cacheDir, final PonyAnimationListing animation) throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException
{
final File ponyDir = new File(dataDir.getAbsolutePath() + File.separator + "ponies");
if (!ponyDir.exists())
{
ponyDir.mkdir();
}
final File tmpFile = new File(cacheDir.getAbsolutePath() + File.separator + "delme.zip");
final URLConnection connection = new URL(animation.getUrl()).openConnection();
connection.connect();
IOUtils.copyStreamAndClose(connection.getInputStream(), new FileOutputStream(tmpFile));
if (!verifyData(new FileInputStream(tmpFile), animation.getChecksum()))
{
throw new IllegalStateException("Signature verification failed.");
}
final File animDir = new File(ponyDir.getAbsolutePath() + File.separator + animation.getId());
if (animDir.exists())
{
for (File file : animDir.listFiles())
{
file.delete();
}
}
else
{
animDir.mkdir();
}
final InputStream istr = new FileInputStream(tmpFile);
final ZipInputStream zis = new ZipInputStream(istr);
ZipEntry zipEntry = null;
while ((zipEntry = zis.getNextEntry()) != null)
{
final FileOutputStream fos = new FileOutputStream(animDir.getAbsolutePath() + File.separator + zipEntry.getName());
IOUtils.copyStream(zis, fos);
fos.close();
}
zis.close();
istr.close();
tmpFile.delete();
}
public static List<PonyAnimationContainer> getPonyAnimations(final File dataDir, final File cacheDir,
final boolean loadAnimations)
throws FileNotFoundException, IOException, ClassNotFoundException, IllegalAccessException, InstantiationException
{
final List<PonyAnimationContainer> containers = new ArrayList<PonyAnimationContainer>();
final File ponyDir = new File(dataDir.getAbsolutePath() + File.separator + "ponies");
if (!ponyDir.exists())
{
return containers;
}
for (final File subDir : ponyDir.listFiles())
{
if (subDir.isDirectory())
{
final File manifest = new File(subDir.getAbsolutePath() + File.separator + "manifest.properties");
if (!manifest.exists())
{
continue;
}
final File lib = new File(subDir.getAbsolutePath() + File.separator + "lib.jar");
if (!lib.exists())
{
continue;
}
final Properties properties = new Properties();
properties.load(new FileInputStream(manifest));
final PonyAnimationContainer container = new PonyAnimationContainer();
container.setId(properties.getProperty("id"));
container.setName(properties.getProperty("name"));
container.setVersion(Long.parseLong(properties.getProperty("version")));
if (loadAnimations)
{
final File animCacheDir = new File(cacheDir.getAbsolutePath() + File.separator + container.getId());
if (!animCacheDir.exists())
{
animCacheDir.mkdir();
}
final DexClassLoader classLoader = new DexClassLoader(lib.getAbsolutePath(),
animCacheDir.getAbsolutePath(), null, PonyDownloader.class.getClassLoader());
Class<?> animClass = classLoader.loadClass(properties.getProperty("className"));
container.setPonyAnimation(animClass.asSubclass(PonyAnimation.class).newInstance());
container.getPonyAnimation().setResourceDir(subDir);
}
containers.add(container);
}
}
return containers;
}
}

@ -0,0 +1,62 @@
package com.derpfish.pinkielive.preference;
import com.derpfish.pinkielive.R;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Color;
import android.preference.DialogPreference;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
public class CreditsPreference extends DialogPreference
{
private View view = null;
public CreditsPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
initialize();
}
public CreditsPreference(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
initialize();
}
private void initialize()
{
this.setWidgetLayoutResource(0);
}
@Override
public void onPrepareDialogBuilder(final AlertDialog.Builder builder)
{
super.onPrepareDialogBuilder(builder);
if (view == null)
{
final TextView textView = new TextView(getContext());
textView.setBackgroundColor(Color.WHITE);
textView.setTextSize(18.0f);
textView.setText(Html.fromHtml(getContext().getString(R.string.credits_string)));
textView.setMovementMethod(LinkMovementMethod.getInstance());
final ScrollView scrollView = new ScrollView(getContext());
scrollView.setFillViewport(true);
scrollView.addView(textView);
view = scrollView;
}
builder.setView(view);
builder.setIcon(0);
builder.setPositiveButton(null, null);
builder.setNegativeButton(null, null);
}
}

@ -0,0 +1,93 @@
package com.derpfish.pinkielive.preference;
import com.derpfish.pinkielive.PinkiePieLiveWallpaper;
import com.derpfish.pinkielive.R;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
public class PinkiePieLiveWallpaperSettings extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener, OnPreferenceClickListener
{
private static final int SELECT_PICTURE = 1;
private Intent gallery;
@Override
protected void onCreate(Bundle icicle)
{
super.onCreate(icicle);
getPreferenceManager().setSharedPreferencesName(PinkiePieLiveWallpaper.SHARED_PREFS_NAME);
addPreferencesFromResource(R.xml.livewallpaper_settings);
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
setDefaultBgEnabled();
findPreference("livewallpaper_image").setOnPreferenceClickListener(this);
gallery = new Intent();
gallery.setType("image/*");
gallery.setAction(Intent.ACTION_GET_CONTENT);
}
private void setDefaultBgEnabled()
{
final SharedPreferences sharedPr = getPreferenceManager().getSharedPreferences();
findPreference("livewallpaper_defaultbg").setEnabled(
!sharedPr.getBoolean("livewallpaper_defaultbg", true)
|| sharedPr.getString("livewallpaper_image", null) != null);
}
@Override
protected void onResume()
{
super.onResume();
}
@Override
protected void onDestroy()
{
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
{
setDefaultBgEnabled();
}
@Override
public boolean onPreferenceClick(Preference pr)
{
if (pr.getKey().equals("livewallpaper_image"))
{
startActivityForResult(Intent.createChooser(gallery, "Select Background"), SELECT_PICTURE);
}
return false;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (resultCode == RESULT_OK)
{
if (requestCode == SELECT_PICTURE)
{
Uri selectedImageUri = data.