Adding original code from https://code.google.com/p/pinkie-live/
This commit is contained in:
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.getData();
|
||||
SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
|
||||
editor.putString("livewallpaper_image", selectedImageUri.toString());
|
||||
editor.putBoolean("livewallpaper_defaultbg", false);
|
||||
editor.commit();
|
||||
((CheckBoxPreference) findPreference("livewallpaper_defaultbg")).setChecked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.derpfish.pinkielive.preference;
|
||||
|
||||
import com.derpfish.pinkielive.PinkiePieLiveWallpaper;
|
||||
import com.derpfish.pinkielive.R;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
|
||||
public class PinkiePieLiveWallpaperSettingsAdvanced extends PreferenceActivity
|
||||
{
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle)
|
||||
{
|
||||
super.onCreate(icicle);
|
||||
|
||||
getPreferenceManager().setSharedPreferencesName(PinkiePieLiveWallpaper.SHARED_PREFS_NAME);
|
||||
addPreferencesFromResource(R.xml.livewallpaper_settings_advanced);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
package com.derpfish.pinkielive.preference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.derpfish.pinkielive.R;
|
||||
import com.derpfish.pinkielive.download.PonyAnimationContainer;
|
||||
import com.derpfish.pinkielive.download.PonyAnimationListing;
|
||||
import com.derpfish.pinkielive.download.PonyDownloader;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class PonyDownloadListPreference extends DialogPreference implements View.OnClickListener
|
||||
{
|
||||
public PonyDownloadListPreference(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public PonyDownloadListPreference(Context context, AttributeSet attrs, int defStyle)
|
||||
{
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private Map<String, PonyAnimationContainer> currentPonies = new HashMap<String, PonyAnimationContainer>();
|
||||
private List<PonyAnimationListing> newListings = new ArrayList<PonyAnimationListing>();
|
||||
|
||||
@Override
|
||||
protected void onClick()
|
||||
{
|
||||
new AsyncTask<Void, Void, Void>()
|
||||
{
|
||||
private ProgressDialog dialog;
|
||||
private List<PonyAnimationContainer> currentPonies;
|
||||
private List<PonyAnimationListing> animListings;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute()
|
||||
{
|
||||
dialog = ProgressDialog.show(getContext(), "", "Loading, please wait...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... arg0)
|
||||
{
|
||||
try
|
||||
{
|
||||
currentPonies = PonyDownloader.getPonyAnimations(
|
||||
getContext().getFilesDir(), getContext().getCacheDir(), false);
|
||||
animListings = PonyDownloader.fetchListings();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result)
|
||||
{
|
||||
dialog.dismiss();
|
||||
doRealOnclick(currentPonies, animListings);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void doRealOnclick(final List<PonyAnimationContainer> currentPonies, final List<PonyAnimationListing> animListings)
|
||||
{
|
||||
this.currentPonies.clear();
|
||||
for (final PonyAnimationContainer pac : currentPonies)
|
||||
{
|
||||
this.currentPonies.put(pac.getId(), pac);
|
||||
}
|
||||
newListings.clear();
|
||||
for (final PonyAnimationListing pal : animListings)
|
||||
{
|
||||
if (!this.currentPonies.containsKey(pal.getId())
|
||||
|| this.currentPonies.get(pal.getId()).getVersion() < pal.getVersion())
|
||||
{
|
||||
newListings.add(pal);
|
||||
}
|
||||
}
|
||||
|
||||
super.onClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareDialogBuilder(final AlertDialog.Builder builder)
|
||||
{
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
|
||||
final ListView listView = new ListView(getContext());
|
||||
listView.setAdapter(new PonyAnimationsAdapter(getContext(), 0, newListings));
|
||||
listView.setBackgroundColor(Color.WHITE);
|
||||
|
||||
builder.setView(listView);
|
||||
builder.setPositiveButton(null, null);
|
||||
}
|
||||
|
||||
private class PonyAnimationsAdapter extends ArrayAdapter<PonyAnimationListing> implements ListAdapter
|
||||
{
|
||||
|
||||
public PonyAnimationsAdapter(Context context, int textViewResourceId, List<PonyAnimationListing> objects)
|
||||
{
|
||||
super(context, textViewResourceId, objects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
final View view;
|
||||
if (convertView == null)
|
||||
{
|
||||
view = View.inflate(getContext(), R.layout.pony_listing, null);
|
||||
} else
|
||||
{
|
||||
view = convertView;
|
||||
}
|
||||
view.setId(position);
|
||||
((TextView)view.findViewById(R.id.title)).setText(this.getItem(position).getName());
|
||||
|
||||
if (!currentPonies.containsKey(newListings.get(position).getId()))
|
||||
{
|
||||
((TextView)view.findViewById(R.id.summary)).setText("New Pony!");
|
||||
}
|
||||
else
|
||||
{
|
||||
((TextView)view.findViewById(R.id.summary)).setText("Update Available!");
|
||||
}
|
||||
view.setOnClickListener(PonyDownloadListPreference.this);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onClick(View view)
|
||||
{
|
||||
final int which = view.getId();
|
||||
new AsyncTask<Void, Void, Void>()
|
||||
{
|
||||
private ProgressDialog dialog;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute()
|
||||
{
|
||||
dialog = ProgressDialog.show(getContext(), "", "Downloading...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... arg0)
|
||||
{
|
||||
try
|
||||
{
|
||||
PonyDownloader.fetchPony(getContext().getFilesDir(), getContext().getCacheDir(), newListings.get(which));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result)
|
||||
{
|
||||
dialog.dismiss();
|
||||
// Dismiss the list of choices
|
||||
getDialog().dismiss();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package com.derpfish.pinkielive.preference;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.derpfish.pinkielive.download.PonyAnimationContainer;
|
||||
import com.derpfish.pinkielive.download.PonyDownloader;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class PonyListPreference extends ListPreference
|
||||
{
|
||||
public PonyListPreference(Context context)
|
||||
{
|
||||
super(context);
|
||||
}
|
||||
|
||||
public PonyListPreference(Context context, AttributeSet attributeSet)
|
||||
{
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(final AlertDialog.Builder builder)
|
||||
{
|
||||
// Setup the dynamic pony list
|
||||
final List<PonyAnimationContainer> animations;
|
||||
try
|
||||
{
|
||||
animations = PonyDownloader.getPonyAnimations(
|
||||
getContext().getFilesDir(), getContext().getCacheDir(), false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
final String[] entries = new String[animations.size()+1];
|
||||
final String[] entryValues = new String[animations.size()+1];
|
||||
entries[0] = "Pinkie Pie";
|
||||
entryValues[0] = "pinkie";
|
||||
for (int i = 0; i < animations.size(); i++)
|
||||
{
|
||||
entries[i+1] = animations.get(i).getName();
|
||||
entryValues[i+1] = animations.get(i).getId();
|
||||
}
|
||||
this.setEntries(entries);
|
||||
this.setEntryValues(entryValues);
|
||||
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,742 @@
|
|||
// https://github.com/android/platform_frameworks_base/raw/master/core/java/android/util/Base64.java
|
||||
/*
|
||||
* Copyright (C) 2010 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.derpfish.pinkielive.util;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* Utilities for encoding and decoding the Base64 representation of
|
||||
* binary data. See RFCs <a
|
||||
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
|
||||
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
|
||||
*/
|
||||
public class Base64 {
|
||||
/**
|
||||
* Default values for encoder/decoder flags.
|
||||
*/
|
||||
public static final int DEFAULT = 0;
|
||||
|
||||
/**
|
||||
* Encoder flag bit to omit the padding '=' characters at the end
|
||||
* of the output (if any).
|
||||
*/
|
||||
public static final int NO_PADDING = 1;
|
||||
|
||||
/**
|
||||
* Encoder flag bit to omit all line terminators (i.e., the output
|
||||
* will be on one long line).
|
||||
*/
|
||||
public static final int NO_WRAP = 2;
|
||||
|
||||
/**
|
||||
* Encoder flag bit to indicate lines should be terminated with a
|
||||
* CRLF pair instead of just an LF. Has no effect if {@code
|
||||
* NO_WRAP} is specified as well.
|
||||
*/
|
||||
public static final int CRLF = 4;
|
||||
|
||||
/**
|
||||
* Encoder/decoder flag bit to indicate using the "URL and
|
||||
* filename safe" variant of Base64 (see RFC 3548 section 4) where
|
||||
* {@code -} and {@code _} are used in place of {@code +} and
|
||||
* {@code /}.
|
||||
*/
|
||||
public static final int URL_SAFE = 8;
|
||||
|
||||
/**
|
||||
* Flag to pass to {@link Base64OutputStream} to indicate that it
|
||||
* should not close the output stream it is wrapping when it
|
||||
* itself is closed.
|
||||
*/
|
||||
public static final int NO_CLOSE = 16;
|
||||
|
||||
// --------------------------------------------------------
|
||||
// shared code
|
||||
// --------------------------------------------------------
|
||||
|
||||
/* package */ static abstract class Coder {
|
||||
public byte[] output;
|
||||
public int op;
|
||||
|
||||
/**
|
||||
* Encode/decode another block of input data. this.output is
|
||||
* provided by the caller, and must be big enough to hold all
|
||||
* the coded data. On exit, this.opwill be set to the length
|
||||
* of the coded data.
|
||||
*
|
||||
* @param finish true if this is the final call to process for
|
||||
* this object. Will finalize the coder state and
|
||||
* include any final bytes in the output.
|
||||
*
|
||||
* @return true if the input so far is good; false if some
|
||||
* error has been detected in the input stream..
|
||||
*/
|
||||
public abstract boolean process(byte[] input, int offset, int len, boolean finish);
|
||||
|
||||
/**
|
||||
* @return the maximum number of bytes a call to process()
|
||||
* could produce for the given number of input bytes. This may
|
||||
* be an overestimate.
|
||||
*/
|
||||
public abstract int maxOutputSize(int len);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// decoding
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
* <p>The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param str the input String to decode, which is converted to
|
||||
* bytes using the default charset
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass {@code DEFAULT} to decode standard Base64.
|
||||
*
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
public static byte[] decode(String str, int flags) {
|
||||
return decode(str.getBytes(), flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
* <p>The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param input the input array to decode
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass {@code DEFAULT} to decode standard Base64.
|
||||
*
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
public static byte[] decode(byte[] input, int flags) {
|
||||
return decode(input, 0, input.length, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
* <p>The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param input the data to decode
|
||||
* @param offset the position within the input array at which to start
|
||||
* @param len the number of bytes of input to decode
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass {@code DEFAULT} to decode standard Base64.
|
||||
*
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
public static byte[] decode(byte[] input, int offset, int len, int flags) {
|
||||
// Allocate space for the most data the input could represent.
|
||||
// (It could contain less if it contains whitespace, etc.)
|
||||
Decoder decoder = new Decoder(flags, new byte[len*3/4]);
|
||||
|
||||
if (!decoder.process(input, offset, len, true)) {
|
||||
throw new IllegalArgumentException("bad base-64");
|
||||
}
|
||||
|
||||
// Maybe we got lucky and allocated exactly enough output space.
|
||||
if (decoder.op == decoder.output.length) {
|
||||
return decoder.output;
|
||||
}
|
||||
|
||||
// Need to shorten the array, so allocate a new one of the
|
||||
// right size and copy.
|
||||
byte[] temp = new byte[decoder.op];
|
||||
System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
|
||||
return temp;
|
||||
}
|
||||
|
||||
/* package */ static class Decoder extends Coder {
|
||||
/**
|
||||
* Lookup table for turning bytes into their position in the
|
||||
* Base64 alphabet.
|
||||
*/
|
||||
private static final int DECODE[] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode lookup table for the "web safe" variant (RFC 3548
|
||||
* sec. 4) where - and _ replace + and /.
|
||||
*/
|
||||
private static final int DECODE_WEBSAFE[] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
};
|
||||
|
||||
/** Non-data values in the DECODE arrays. */
|
||||
private static final int SKIP = -1;
|
||||
private static final int EQUALS = -2;
|
||||
|
||||
/**
|
||||
* States 0-3 are reading through the next input tuple.
|
||||
* State 4 is having read one '=' and expecting exactly
|
||||
* one more.
|
||||
* State 5 is expecting no more data or padding characters
|
||||
* in the input.
|
||||
* State 6 is the error state; an error has been detected
|
||||
* in the input and no future input can "fix" it.
|
||||
*/
|
||||
private int state; // state number (0 to 6)
|
||||
private int value;
|
||||
|
||||
final private int[] alphabet;
|
||||
|
||||
public Decoder(int flags, byte[] output) {
|
||||
this.output = output;
|
||||
|
||||
alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
|
||||
state = 0;
|
||||
value = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an overestimate for the number of bytes {@code
|
||||
* len} bytes could decode to.
|
||||
*/
|
||||
public int maxOutputSize(int len) {
|
||||
return len * 3/4 + 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode another block of input data.
|
||||
*
|
||||
* @return true if the state machine is still healthy. false if
|
||||
* bad base-64 data has been detected in the input stream.
|
||||
*/
|
||||
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||
if (this.state == 6) return false;
|
||||
|
||||
int p = offset;
|
||||
len += offset;
|
||||
|
||||
// Using local variables makes the decoder about 12%
|
||||
// faster than if we manipulate the member variables in
|
||||
// the loop. (Even alphabet makes a measurable
|
||||
// difference, which is somewhat surprising to me since
|
||||
// the member variable is final.)
|
||||
int state = this.state;
|
||||
int value = this.value;
|
||||
int op = 0;
|
||||
final byte[] output = this.output;
|
||||
final int[] alphabet = this.alphabet;
|
||||
|
||||
while (p < len) {
|
||||
// Try the fast path: we're starting a new tuple and the
|
||||
// next four bytes of the input stream are all data
|
||||
// bytes. This corresponds to going through states
|
||||
// 0-1-2-3-0. We expect to use this method for most of
|
||||
// the data.
|
||||
//
|
||||
// If any of the next four bytes of input are non-data
|
||||
// (whitespace, etc.), value will end up negative. (All
|
||||
// the non-data values in decode are small negative
|
||||
// numbers, so shifting any of them up and or'ing them
|
||||
// together will result in a value with its top bit set.)
|
||||
//
|
||||
// You can remove this whole block and the output should
|
||||
// be the same, just slower.
|
||||
if (state == 0) {
|
||||
while (p+4 <= len &&
|
||||
(value = ((alphabet[input[p] & 0xff] << 18) |
|
||||
(alphabet[input[p+1] & 0xff] << 12) |
|
||||
(alphabet[input[p+2] & 0xff] << 6) |
|
||||
(alphabet[input[p+3] & 0xff]))) >= 0) {
|
||||
output[op+2] = (byte) value;
|
||||
output[op+1] = (byte) (value >> 8);
|
||||
output[op] = (byte) (value >> 16);
|
||||
op += 3;
|
||||
p += 4;
|
||||
}
|
||||
if (p >= len) break;
|
||||
}
|
||||
|
||||
// The fast path isn't available -- either we've read a
|
||||
// partial tuple, or the next four input bytes aren't all
|
||||
// data, or whatever. Fall back to the slower state
|
||||
// machine implementation.
|
||||
|
||||
int d = alphabet[input[p++] & 0xff];
|
||||
|
||||
switch (state) {
|
||||
case 0:
|
||||
if (d >= 0) {
|
||||
value = d;
|
||||
++state;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (d >= 0) {
|
||||
value = (value << 6) | d;
|
||||
++state;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (d >= 0) {
|
||||
value = (value << 6) | d;
|
||||
++state;
|
||||
} else if (d == EQUALS) {
|
||||
// Emit the last (partial) output tuple;
|
||||
// expect exactly one more padding character.
|
||||
output[op++] = (byte) (value >> 4);
|
||||
state = 4;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (d >= 0) {
|
||||
// Emit the output triple and return to state 0.
|
||||
value = (value << 6) | d;
|
||||
output[op+2] = (byte) value;
|
||||
output[op+1] = (byte) (value >> 8);
|
||||
output[op] = (byte) (value >> 16);
|
||||
op += 3;
|
||||
state = 0;
|
||||
} else if (d == EQUALS) {
|
||||
// Emit the last (partial) output tuple;
|
||||
// expect no further data or padding characters.
|
||||
output[op+1] = (byte) (value >> 2);
|
||||
output[op] = (byte) (value >> 10);
|
||||
op += 2;
|
||||
state = 5;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
if (d == EQUALS) {
|
||||
++state;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 5:
|
||||
if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finish) {
|
||||
// We're out of input, but a future call could provide
|
||||
// more.
|
||||
this.state = state;
|
||||
this.value = value;
|
||||
this.op = op;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Done reading input. Now figure out where we are left in
|
||||
// the state machine and finish up.
|
||||
|
||||
switch (state) {
|
||||
case 0:
|
||||
// Output length is a multiple of three. Fine.
|
||||
break;
|
||||
case 1:
|
||||
// Read one extra input byte, which isn't enough to
|
||||
// make another output byte. Illegal.
|
||||
this.state = 6;
|
||||
return false;
|
||||
case 2:
|
||||
// Read two extra input bytes, enough to emit 1 more
|
||||
// output byte. Fine.
|
||||
output[op++] = (byte) (value >> 4);
|
||||
break;
|
||||
case 3:
|
||||
// Read three extra input bytes, enough to emit 2 more
|
||||
// output bytes. Fine.
|
||||
output[op++] = (byte) (value >> 10);
|
||||
output[op++] = (byte) (value >> 2);
|
||||
break;
|
||||
case 4:
|
||||
// Read one padding '=' when we expected 2. Illegal.
|
||||
this.state = 6;
|
||||
return false;
|
||||
case 5:
|
||||
// Read all the padding '='s we expected and no more.
|
||||
// Fine.
|
||||
break;
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
this.op = op;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// encoding
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* String with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static String encodeToString(byte[] input, int flags) {
|
||||
try {
|
||||
return new String(encode(input, flags), "US-ASCII");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// US-ASCII is guaranteed to be available.
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* String with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param offset the position within the input array at which to
|
||||
* start
|
||||
* @param len the number of bytes of input to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static String encodeToString(byte[] input, int offset, int len, int flags) {
|
||||
try {
|
||||
return new String(encode(input, offset, len, flags), "US-ASCII");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// US-ASCII is guaranteed to be available.
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* byte[] with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static byte[] encode(byte[] input, int flags) {
|
||||
return encode(input, 0, input.length, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* byte[] with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param offset the position within the input array at which to
|
||||
* start
|
||||
* @param len the number of bytes of input to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static byte[] encode(byte[] input, int offset, int len, int flags) {
|
||||
Encoder encoder = new Encoder(flags, null);
|
||||
|
||||
// Compute the exact length of the array we will produce.
|
||||
int output_len = len / 3 * 4;
|
||||
|
||||
// Account for the tail of the data and the padding bytes, if any.
|
||||
if (encoder.do_padding) {
|
||||
if (len % 3 > 0) {
|
||||
output_len += 4;
|
||||
}
|
||||
} else {
|
||||
switch (len % 3) {
|
||||
case 0: break;
|
||||
case 1: output_len += 2; break;
|
||||
case 2: output_len += 3; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Account for the newlines, if any.
|
||||
if (encoder.do_newline && len > 0) {
|
||||
output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
|
||||
(encoder.do_cr ? 2 : 1);
|
||||
}
|
||||
|
||||
encoder.output = new byte[output_len];
|
||||
encoder.process(input, offset, len, true);
|
||||
|
||||
assert encoder.op == output_len;
|
||||
|
||||
return encoder.output;
|
||||
}
|
||||
|
||||
/* package */ static class Encoder extends Coder {
|
||||
/**
|
||||
* Emit a new line every this many output tuples. Corresponds to
|
||||
* a 76-character line length (the maximum allowable according to
|
||||
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
|
||||
*/
|
||||
public static final int LINE_GROUPS = 19;
|
||||
|
||||
/**
|
||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||
* into output bytes.
|
||||
*/
|
||||
private static final byte ENCODE[] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
|
||||
};
|
||||
|
||||
/**
|
||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||
* into output bytes.
|
||||
*/
|
||||
private static final byte ENCODE_WEBSAFE[] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
|
||||
};
|
||||
|
||||
final private byte[] tail;
|
||||
/* package */ int tailLen;
|
||||
private int count;
|
||||
|
||||
final public boolean do_padding;
|
||||
final public boolean do_newline;
|
||||
final public boolean do_cr;
|
||||
final private byte[] alphabet;
|
||||
|
||||
public Encoder(int flags, byte[] output) {
|
||||
this.output = output;
|
||||
|
||||
do_padding = (flags & NO_PADDING) == 0;
|
||||
do_newline = (flags & NO_WRAP) == 0;
|
||||
do_cr = (flags & CRLF) != 0;
|
||||
alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
|
||||
|
||||
tail = new byte[2];
|
||||
tailLen = 0;
|
||||
|
||||
count = do_newline ? LINE_GROUPS : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an overestimate for the number of bytes {@code
|
||||
* len} bytes could encode to.
|
||||
*/
|
||||
public int maxOutputSize(int len) {
|
||||
return len * 8/5 + 10;
|
||||
}
|
||||
|
||||
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||
// Using local variables makes the encoder about 9% faster.
|
||||
final byte[] alphabet = this.alphabet;
|
||||
final byte[] output = this.output;
|
||||
int op = 0;
|
||||
int count = this.count;
|
||||
|
||||
int p = offset;
|
||||
len += offset;
|
||||
int v = -1;
|
||||
|
||||
// First we need to concatenate the tail of the previous call
|
||||
// with any input bytes available now and see if we can empty
|
||||
// the tail.
|
||||
|
||||
switch (tailLen) {
|
||||
case 0:
|
||||
// There was no tail.
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (p+2 <= len) {
|
||||
// A 1-byte tail with at least 2 bytes of
|
||||
// input available now.
|
||||
v = ((tail[0] & 0xff) << 16) |
|
||||
((input[p++] & 0xff) << 8) |
|
||||
(input[p++] & 0xff);
|
||||
tailLen = 0;
|
||||
};
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (p+1 <= len) {
|
||||
// A 2-byte tail with at least 1 byte of input.
|
||||
v = ((tail[0] & 0xff) << 16) |
|
||||
((tail[1] & 0xff) << 8) |
|
||||
(input[p++] & 0xff);
|
||||
tailLen = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (v != -1) {
|
||||
output[op++] = alphabet[(v >> 18) & 0x3f];
|
||||
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op++] = alphabet[v & 0x3f];
|
||||
if (--count == 0) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
count = LINE_GROUPS;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point either there is no tail, or there are fewer
|
||||
// than 3 bytes of input available.
|
||||
|
||||
// The main loop, turning 3 input bytes into 4 output bytes on
|
||||
// each iteration.
|
||||
while (p+3 <= len) {
|
||||
v = ((input[p] & 0xff) << 16) |
|
||||
((input[p+1] & 0xff) << 8) |
|
||||
(input[p+2] & 0xff);
|
||||
output[op] = alphabet[(v >> 18) & 0x3f];
|
||||
output[op+1] = alphabet[(v >> 12) & 0x3f];
|
||||
output[op+2] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op+3] = alphabet[v & 0x3f];
|
||||
p += 3;
|
||||
op += 4;
|
||||
if (--count == 0) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
count = LINE_GROUPS;
|
||||
}
|
||||
}
|
||||
|
||||
if (finish) {
|
||||
// Finish up the tail of the input. Note that we need to
|
||||
// consume any bytes in tail before any bytes
|
||||
// remaining in input; there should be at most two bytes
|
||||
// total.
|
||||
|
||||
if (p-tailLen == len-1) {
|
||||
int t = 0;
|
||||
v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
|
||||
tailLen -= t;
|
||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op++] = alphabet[v & 0x3f];
|
||||
if (do_padding) {
|
||||
output[op++] = '=';
|
||||
output[op++] = '=';
|
||||
}
|
||||
if (do_newline) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
}
|
||||
} else if (p-tailLen == len-2) {
|
||||
int t = 0;
|
||||
v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
|
||||
(((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
|
||||
tailLen -= t;
|
||||
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op++] = alphabet[v & 0x3f];
|
||||
if (do_padding) {
|
||||
output[op++] = '=';
|
||||
}
|
||||
if (do_newline) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
}
|
||||
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
}
|
||||
|
||||
assert tailLen == 0;
|
||||
assert p == len;
|
||||
} else {
|
||||
// Save the leftovers in tail to be consumed on the next
|
||||
// call to encodeInternal.
|
||||
|
||||
if (p == len-1) {
|
||||
tail[tailLen++] = input[p];
|
||||
} else if (p == len-2) {
|
||||
tail[tailLen++] = input[p];
|
||||
tail[tailLen++] = input[p+1];
|
||||
}
|
||||
}
|
||||
|
||||
this.op = op;
|
||||
this.count = count;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private Base64() { } // don't instantiate
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.derpfish.pinkielive.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class IOUtils
|
||||
{
|
||||
/**
|
||||
* Copies the bytes from istream to ostream
|
||||
*
|
||||
* @param istream
|
||||
* @param ostream
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void copyStream(final InputStream istream, final OutputStream ostream) throws IOException
|
||||
{
|
||||
final byte[] buffer = new byte[4096];
|
||||
int nread = 0;
|
||||
while ((nread = istream.read(buffer)) >= 0)
|
||||
{
|
||||
ostream.write(buffer, 0, nread);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the bytes from istream to stream, closing both streams when the end of istream
|
||||
* is reached.
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void copyStreamAndClose(final InputStream istream, final OutputStream ostream) throws IOException
|
||||
{
|
||||
copyStream(istream, ostream);
|
||||
istream.close();
|
||||
ostream.close();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue