This commit is contained in:
Michel Roux 2014-08-02 11:32:24 +02:00
parent 9e6e4b8311
commit 6fb13ea8da
36 changed files with 2514 additions and 0 deletions

49
AndroidManifest.xml Normal file
View File

@ -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>

BIN
assets/defaultbg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
assets/jump1.zip Normal file

Binary file not shown.

BIN
assets/jump2.zip Normal file

Binary file not shown.

BIN
assets/jump3.zip Normal file

Binary file not shown.

BIN
assets/jump4.zip Normal file

Binary file not shown.

BIN
assets/jump5.zip Normal file

Binary file not shown.

BIN
assets/jump6.zip Normal file

Binary file not shown.

BIN
assets/jump7.zip Normal file

Binary file not shown.

BIN
ic_launcher-web.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
libs/android-support-v4.jar Normal file

Binary file not shown.

20
proguard-project.txt Normal file
View File

@ -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 *;
#}

14
project.properties Normal file
View File

@ -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

View File

@ -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>

16
res/values/strings.xml Normal file
View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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)));
}
}

View File

@ -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);
}
}
}
}

View File

@ -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)
{
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}