This commit is contained in:
seven332 committed 2015-03-07 15:56:43 +08:00
commit fd5d07b2f2
658 files changed
+57526

No files matched your search

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
import android.content.Context;
import com.google.analytics.tracking.android.EasyTracker;
import com.google.analytics.tracking.android.MapBuilder;
import com.hippo.ehviewer.util.Config;
import org.json.JSONObject;
public class Analytics {
private static final String KEY_GALLERY = "gallery";
private static final String KEY_OPEN = "open";
private static final String KEY_READ = "read";
private static final String KEY_DOWNLOAD = "download";
private static final String KEY_ADD_TO_FAVORITE = "add_to_favorite";
private static void doGallery(Context context, int gid, String token, String method) {
if (!Config.getAllowAnalyics())
return;
try {
JSONObject js = new JSONObject();
js.put("gid", gid).put("token", token);
EasyTracker easyTracker = EasyTracker.getInstance(context);
easyTracker.send(MapBuilder.createEvent(KEY_GALLERY, method, js.toString(), null).build());
} catch (Exception e) {}
}
public static void openGallery(Context context, int gid, String token) {
doGallery(context, gid, token, KEY_OPEN);
}
public static void readGallery(Context context, int gid, String token) {
doGallery(context, gid, token, KEY_READ);
}
public static void downloadGallery(Context context, int gid, String token) {
doGallery(context, gid, token, KEY_DOWNLOAD);
}
public static void addToFavoriteGallery(Context context, int gid, String token) {
doGallery(context, gid, token, KEY_ADD_TO_FAVORITE);
}
}
@@ -0,0 +1,174 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Typeface;
import android.view.ContextThemeWrapper;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.data.Data;
import com.hippo.ehviewer.ehclient.EhClient;
import com.hippo.ehviewer.ehclient.EhInfo;
import com.hippo.ehviewer.ehclient.ExDownloaderManager;
import com.hippo.ehviewer.network.HttpHelper;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Crash;
import com.hippo.ehviewer.util.EhUtils;
import com.hippo.ehviewer.util.Favorite;
import com.hippo.ehviewer.util.Log;
import com.hippo.ehviewer.util.Ui;
import com.hippo.ehviewer.util.Utils;
import com.hippo.ehviewer.util.ViewUtils;
import com.hippo.ehviewer.widget.MaterialToast;
import com.hippo.ehviewer.widget.SlidingDrawerLayout;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
public class AppContext extends Application implements UncaughtExceptionHandler {
@SuppressWarnings("unused")
private static final String TAG = AppContext.class.getSimpleName();
private Thread.UncaughtExceptionHandler mDefaultHandler;
private Typeface mFaceTypeface;
@Override
public void onCreate() {
super.onCreate();
// Do catch error prepare
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
Context mContextThemeWrapper = new ContextThemeWrapper(this, R.style.AppTheme);
ViewUtils.initDialogViewInflater(this);
// Init everything
Ui.init(mContextThemeWrapper);
Config.init(mContextThemeWrapper);
Log.init(mContextThemeWrapper);
Crash.init(mContextThemeWrapper);
EhClient.createInstance(mContextThemeWrapper);
Favorite.init(mContextThemeWrapper);
Data.createInstance(mContextThemeWrapper);
ExDownloaderManager.createInstance(mContextThemeWrapper);
MaterialToast.setContext(mContextThemeWrapper);
mFaceTypeface = Typeface.createFromAsset(getAssets(),"fonts/face.ttf");
// Set drawer margin
SlidingDrawerLayout.setDefaultMinDrawerMargin(Ui.ACTION_BAR_HEIGHT * 2);
// Add .nomedia or delete it
File nomedia = new File(Config.getDownloadPath(), ".nomedia");
if (Config.getMediaScan()) {
nomedia.delete();
} else {
try {
new FileOutputStream(nomedia).close();
} catch (IOException e) {}
}
// Update proxy urls
HttpHelper.updateProxyUrls(mContextThemeWrapper);
String keyVersionCode = "version_code";
int oldVersion = Config.getInt(keyVersionCode, Integer.MAX_VALUE);
// Fix <=22 login error
if (oldVersion <= 22)
EhInfo.getInstance(mContextThemeWrapper).logout();
// Fix <=25 Update
if (oldVersion <= 25) {
File downloadDir = new File(Config.getDownloadPath());
String cachePath = getExternalCacheDir() != null
? getExternalCacheDir().getPath()
: getCacheDir().getPath();
File oldEdInfoDir = new File(cachePath, "ExDownloaderManager");
String[] list = downloadDir.list();
if (list != null && list.length != 0) {
for (String str : list) {
int index = str.indexOf('-');
if (index == -1)
continue;
String gid = str.substring(0, index);
File gDir = new File(downloadDir, str);
// Move download info file
File oldInfoFile = new File(oldEdInfoDir, gid);
File newInfoFile = new File(gDir, EhUtils.EH_DOWNLOAD_FILENAME);
if (!oldInfoFile.exists())
continue;
try {
FileOutputStream fos = new FileOutputStream(newInfoFile);
// Add last page
byte[] bs = {'0', '0', '0', '0', '0', '0', '0', '0', '\n'};
fos.write(bs);
Utils.copy(new FileInputStream(oldInfoFile), fos);
} catch (Throwable e) {
continue;
}
}
}
// Delete old exdownload info dir
Utils.deleteDirInThread(oldEdInfoDir);
}
// Update version code
try {
PackageInfo pi= getPackageManager().getPackageInfo(getPackageName(), 0);
Config.setInt(keyVersionCode, pi.versionCode);
} catch (NameNotFoundException e) {}
}
/**
* {@inheritDoc}
*/
@Override
public void onLowMemory() {
ImageCache.getInstance(this).evictAll();
super.onLowMemory();
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, ex);
}
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
private boolean handleException(Throwable ex) {
if (ex == null)
return false;
Crash.saveCrashInfo2File(ex);
return true;
}
public Typeface getFaceTypeface() {
return mFaceTypeface;
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.hippo.ehviewer.network.HttpHelper;
import com.hippo.ehviewer.util.Constants;
public class AppHandler extends Handler {
public static final int HTTP_HELPER_TAG = 0x1;
private static Handler sInstance;
public AppHandler(Looper mainLooper) {
super(mainLooper);
}
public final static Handler getInstance() {
if (sInstance == null)
sInstance = new AppHandler(Looper.getMainLooper());
return sInstance;
}
@Override
public void handleMessage(Message msg) {
switch(msg.what) {
case HTTP_HELPER_TAG:
HttpHelper.Package p = (HttpHelper.Package)msg.obj;
HttpHelper.OnRespondListener listener = p.listener;
Object obj = p.obj;
if (msg.arg1 == Constants.TRUE) {
listener.onSuccess(obj);
} else {
listener.onFailure((String)obj);
}
break;
}
}
}
@@ -0,0 +1,181 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
import android.content.Context;
import android.graphics.Bitmap;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.network.HttpHelper;
import com.hippo.ehviewer.util.BgThread;
import java.util.Stack;
public class ImageLoader {
@SuppressWarnings("unused")
private static final String TAG = ImageLoader.class.getSimpleName();
public static final int STATE_NONE = 0;
public static final int STATE_FROM_MEMORY = 1;
public static final int STATE_FROM_DISK = 2;
public static final int STATE_FROM_NETWORK = 3;
private static ImageLoader sInstance;
class LoadTask {
public String url;
public String key;
public OnGetImageListener listener;
public Bitmap bitmap;
public int state;
public LoadTask(String url, String key, OnGetImageListener listener) {
this.url = url;
this.key = key;
this.listener = listener;
}
}
private final Context mContext;
private final Stack<LoadTask> mLoadTasks;
private final ImageCache mImageCache;
private final ImageDownloader mImageDownloader;
private ImageLoader(Context context) {
mLoadTasks = new Stack<LoadTask>();
mImageDownloader = new ImageDownloader();
mContext = context;
mImageCache = ImageCache.getInstance(mContext);
new BgThread(new LoadFromCacheTask()).start();
}
public final static ImageLoader getInstance(final Context context) {
if (sInstance == null) {
sInstance = new ImageLoader(context.getApplicationContext());
}
return sInstance;
}
public void add(String url, String key, OnGetImageListener listener) {
synchronized (mLoadTasks) {
mLoadTasks.push(new LoadTask(url, key, listener));
mLoadTasks.notify();
}
}
private class LoadImageCallback implements Runnable {
private final LoadTask mLoadTask;
private LoadImageCallback(LoadTask task) {
mLoadTask = task;
}
@Override
public void run() {
mLoadTask.listener.onGetImage(mLoadTask.key, mLoadTask.bitmap, mLoadTask.state);
}
}
private class LoadFromCacheTask implements Runnable {
@Override
public void run() {
LoadTask loadTask;
while (true) {
synchronized (mLoadTasks) {
if (mLoadTasks.isEmpty()) {
try {
mLoadTasks.wait();
} catch (InterruptedException e) {}
continue;
}
loadTask = mLoadTasks.pop();
}
int[] state = new int[1];
String key = loadTask.key;
loadTask.bitmap = mImageCache.getCachedBitmap(key, state);
loadTask.state = state[0];
if (loadTask.bitmap != null)
AppHandler.getInstance().post(new LoadImageCallback(loadTask));
else
mImageDownloader.add(loadTask);
}
}
}
private class ImageDownloader {
private static final int MAX_DOWNLOAD_THREADS = 5;
private final Stack<LoadTask> mDownloadTasks;
private int mWorkingThreadNum = 0;
public ImageDownloader() {
mDownloadTasks = new Stack<LoadTask>();
}
public void add(LoadTask loadTask) {
synchronized (mDownloadTasks) {
mDownloadTasks.push(loadTask);
if (mWorkingThreadNum < MAX_DOWNLOAD_THREADS) {
new BgThread(new DownloadImageTask()).start();
mWorkingThreadNum++;
}
}
}
private class DownloadImageTask implements Runnable {
@Override
public void run() {
LoadTask loadTask;
HttpHelper httpHelper = new HttpHelper(mContext);
while (true) {
synchronized (mDownloadTasks) {
if (mDownloadTasks.isEmpty()) {
loadTask = null;
mWorkingThreadNum--;
break;
}
loadTask = mDownloadTasks.pop();
}
// TODO use proxy to get image
loadTask.bitmap = httpHelper.getImage(loadTask.url);
if (loadTask.bitmap != null) {
mImageCache.addBitmapToCache(loadTask.key, loadTask.bitmap);
loadTask.state = STATE_FROM_NETWORK;
} else {
loadTask.state = STATE_NONE;
}
AppHandler.getInstance().post(new LoadImageCallback(loadTask));
}
}
}
}
public interface OnGetImageListener {
/**
* bmp is null for fail
* @param key
* @param bmp
*/
void onGetImage(String key, Bitmap bmp, int state);
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
public class NotificationId {
public static final int UPDATE_ID = -1;
public static final int DOWNLOAD_ID = -2;
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
import android.content.SearchRecentSuggestionsProvider;
public class SimpleSuggestionProvider extends SearchRecentSuggestionsProvider {
public final static String AUTHORITY = "com.hippo.ehviewer.SimpleSuggestionProvider";
public final static int MODE = DATABASE_MODE_QUERIES;
public SimpleSuggestionProvider() {
setupSuggestions(AUTHORITY, MODE);
}
}
@@ -0,0 +1,256 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.text.util.Linkify;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.hippo.ehviewer.app.MaterialAlertDialog;
import com.hippo.ehviewer.network.Network;
import com.hippo.ehviewer.ui.AbsActivity;
import com.hippo.ehviewer.ui.GalleryListActivity;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Crash;
import com.hippo.ehviewer.util.Secret;
import com.hippo.ehviewer.util.ViewUtils;
import com.larvalabs.svgandroid.SVGBuilder;
public class StartActivity extends AbsActivity {
@SuppressWarnings("unused")
private static final String TAG = StartActivity.class.getSimpleName();
private static final int CHECK_WARING = 0;
private static final int CHECK_ANALYTICS = 1;
private static final int CHECK_NETWORK = 2;
private static final int CHECK_CRASH = 3;
private static final String KEY_ALLOWED = "allowed";
private static final String KEY_SET_ANALYTICS = "set_analyics";
private String lastCrash;
private boolean isAnimationOver = false;
private boolean isCheckOver = false;
private MaterialAlertDialog createWarningDialog() {
return new MaterialAlertDialog.Builder(this).setCancelable(false)
.setTitle(R.string.dailog_waring_title)
.setMessage(R.string.dailog_waring_plain)
.setPositiveButton(android.R.string.ok)
.setNegativeButton(android.R.string.cancel)
.setButtonListener(new MaterialAlertDialog.OnClickListener() {
@Override
public boolean onClick(MaterialAlertDialog dialog, int which) {
switch (which) {
case MaterialAlertDialog.POSITIVE:
Config.setBoolean(KEY_ALLOWED, true);
check(CHECK_ANALYTICS);
break;
case MaterialAlertDialog.NEGATIVE:
finish();
break;
}
return true;
}
}).create();
}
private AlertDialog createAllowAnalyicsDialog() {
return new MaterialAlertDialog.Builder(this).setCancelable(false)
.setTitle(R.string.dailog_analyics_title)
.setMessageAutoLink(Linkify.WEB_URLS)
.setMessage(R.string.dailog_analyics_plain)
.setPositiveButton(android.R.string.ok)
.setNegativeButton(android.R.string.cancel)
.setButtonListener(new MaterialAlertDialog.OnClickListener() {
@Override
public boolean onClick(MaterialAlertDialog dialog, int which) {
switch (which) {
case MaterialAlertDialog.POSITIVE:
Config.setAllowAnalyics(true);
check(CHECK_NETWORK);
break;
case MaterialAlertDialog.NEGATIVE:
Config.setAllowAnalyics(false);
check(CHECK_NETWORK);
break;
}
return true;
}
}).create();
}
private AlertDialog createNetworkDialog(int state) {
int mesgId;
switch (state) {
case Network.NETWORK_STATE_MOBILE:
mesgId = R.string.dailog_network_mobile_title;
break;
case Network.NETWORK_STATE_NONE:
default:
mesgId = R.string.dailog_network_none_title;
break;
}
return new MaterialAlertDialog.Builder(this).setCancelable(false)
.setTitle(R.string.warning).setMessage(mesgId)
.setPositiveButton(android.R.string.ok)
.setNegativeButton(android.R.string.cancel)
.setButtonListener(new MaterialAlertDialog.OnClickListener() {
@Override
public boolean onClick(MaterialAlertDialog dialog, int which) {
switch (which) {
case MaterialAlertDialog.POSITIVE:
check(CHECK_CRASH);
break;
case MaterialAlertDialog.NEGATIVE:
finish();
break;
}
return true;
}
}).create();
}
private AlertDialog createSendCrashDialog() {
View view = ViewUtils.inflateDialogView(R.layout.send_crash, false);
final EditText et = (EditText) view.findViewById(R.id.description_of_user);
return new MaterialAlertDialog.Builder(this).setCancelable(false)
.setTitle(R.string.dialog_send_crash_title)
.setView(view, true)
.setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE)
.setButtonListener(new MaterialAlertDialog.OnClickListener() {
@Override
public boolean onClick(MaterialAlertDialog dialog, int which) {
switch (which) {
case MaterialAlertDialog.POSITIVE:
// A wait dialog
new MaterialAlertDialog.Builder(StartActivity.this)
.setCancelable(false)
.setTitle(R.string.wait)
.setMessage(R.string.dialog_wait_send_crash_msg)
.setPositiveButton(android.R.string.ok)
.setButtonListener(
new MaterialAlertDialog.OnClickListener() {
@Override
public boolean onClick(MaterialAlertDialog dialog, int which) {
checkOver();
return true;
}
}).show();
Intent i = new Intent(Intent.ACTION_SENDTO);
i.setData(Uri.parse("mailto:ehviewersu@gmail.com"));
i.putExtra(Intent.EXTRA_SUBJECT, "I found a bug in EhViewer !");
i.putExtra(Intent.EXTRA_TEXT, lastCrash + "======== Description ========\n" + et.getText().toString());
startActivity(i);
lastCrash = null;
break;
case MaterialAlertDialog.NEGATIVE:
lastCrash = null;
checkOver();
break;
}
return true;
}
}).create();
}
@Override
public void onCreate(Bundle savedInstanceState) {
// Disable animate
overridePendingTransition(0, 0);
super.onCreate(savedInstanceState);
setContentView(R.layout.start_activity);
TextView text = (TextView) findViewById(R.id.text);
text.setTypeface(Typeface.createFromAsset(getAssets(),"fonts/GloriaHallelujah.ttf"));
ImageView image = (ImageView) findViewById(R.id.image);
image.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
image.setImageDrawable(new SVGBuilder().readFromResource(getResources(), R.raw.sad_pandroid).build().getDrawable());
// Update secret image here
int state = Network.getNetworkState(this);
if (state == Network.NETWORK_STATE_WIFI) {
Secret.updateSecretImage(this);
}
AppHandler.getInstance().postDelayed(new Runnable() {
@Override
public void run() {
if (isCheckOver)
redirectTo();
isAnimationOver = true;
}
}, 3000);
check(CHECK_WARING);
}
/**
* Order is 1. check waring 2. check analyics 3. check network 4. check
* crash 5. check external storage
*
* @param order
*/
private void check(int order) {
switch (order) {
case CHECK_WARING:
if (!Config.getBoolean(KEY_ALLOWED, false)) {
createWarningDialog().show();
return;
}
case CHECK_ANALYTICS:
if (!Config.getBoolean(KEY_SET_ANALYTICS, false)) {
Config.setBoolean(KEY_SET_ANALYTICS, true);
createAllowAnalyicsDialog().show();
return;
}
case CHECK_NETWORK:
int state = Network.getNetworkState(this);
if (state != Network.NETWORK_STATE_WIFI) {
createNetworkDialog(state).show();
return;
}
case CHECK_CRASH:
if ((lastCrash = Crash.getLastCrash()) != null) {
createSendCrashDialog().show();
return;
}
}
checkOver();
}
private void checkOver() {
if (isAnimationOver)
redirectTo();
isCheckOver = true;
}
private void redirectTo() {
Intent intent = new Intent(this, GalleryListActivity.class);
startActivity(intent);
finish();
}
}
@@ -0,0 +1,258 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer;
import java.io.File;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import com.hippo.ehviewer.ehclient.EhClient;
import com.hippo.ehviewer.network.HttpHelper;
import com.hippo.ehviewer.util.BgThread;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Utils;
// TODO return onFailure when downloading update or check update
// TODO add disable to achieve
public class UpdateHelper implements Runnable {
@SuppressWarnings("unused")
private static final String TAG = "UpdateHelper";
private static final String HEADER = "Ehviewer-";
private static final String FOOTER = ".apk";
private final AppContext mAppContext;
private OnCheckUpdateListener mListener;
private static boolean mEnabled = true;
public interface OnCheckUpdateListener {
public void onSuccess(String version, long size, String url, String fileName, String info);
public void onNoUpdate();
public void onFailure(String eMsg);
}
public UpdateHelper(AppContext appContext) {
mAppContext = appContext;
}
public UpdateHelper SetOnCheckUpdateListener(OnCheckUpdateListener listener) {
this.mListener = listener;
return this;
}
public void autoCheckUpdate() {
if (Config.getUpdateDate() < Utils.getDate())
checkUpdate();
}
public static void setEnabled(boolean enabled) {
mEnabled = enabled;
}
class CheckUpdatePackage {
public OnCheckUpdateListener listener;
public String body;
public String emesg;
}
private void send(CheckUpdatePackage _package) {
AppHandler.getInstance().post(new Respond(_package));
}
@Override
public void run() {
CheckUpdatePackage _package = new CheckUpdatePackage();
_package.listener = mListener;
final HttpHelper hp = new HttpHelper(mAppContext);
if (!mEnabled) {
_package.body = "working";
send(_package);
return;
}
mEnabled = false;
PackageManager pm = mAppContext.getPackageManager();
PackageInfo pi = null;
try {
pi = pm.getPackageInfo(mAppContext.getPackageName(), PackageManager.GET_ACTIVITIES);
JSONObject jo = new JSONObject();
jo.put("method", "update");
JSONObject detailJO = new JSONObject();
detailJO.put("version", pi.versionName);
detailJO.put("server", Config.getUpdateServer());
jo.put("detail", detailJO);
_package.body = hp.postJson(EhClient.API_EHVIEWER, jo);
_package.emesg = hp.getEMsg();
send(_package);
} catch (NameNotFoundException e) {
e.printStackTrace();
_package.body = "NameNotFound";
send(_package);
} catch (JSONException e) {
e.printStackTrace();
_package.body = "JSONException";
send(_package);
}
}
public void checkUpdate() {
new BgThread(this).start();
}
private class Respond implements Runnable {
CheckUpdatePackage mPackage;
public Respond(CheckUpdatePackage _package) {
mPackage = _package;
}
@Override
public void run() {
OnCheckUpdateListener listener = mPackage.listener;
String body = mPackage.body;
if (body != null) {
Config.setUpdateDate();
if (listener == null)
return;
if (body.equals("working")) {
listener.onFailure("正在检查更新或者下载更新"); // TODO
} else if (body.equals("NameNotFound")) {
listener.onFailure("NameNotFound"); // TODO
} else if (body.equals("JSONException")) {
listener.onFailure("JSONException"); // TODO
} else {
try {
JSONObject jo = new JSONObject(body);
JSONObject updateJO = jo.getJSONObject("update");
if (updateJO.has("error")) {
listener.onFailure(updateJO.getString("error"));
} else {
String version = updateJO.getString("version");
if (version.equals("none")) {
listener.onNoUpdate();
} else {
long size = updateJO.getLong("size");
String url = updateJO.getString("url");
String info = updateJO.getString("info");
listener.onSuccess(version, size, url, HEADER + version + FOOTER, info);
}
}
} catch (JSONException e) {
e.printStackTrace();
listener.onFailure("JSONException");
}
}
}
}
}
public static class UpdateListener implements HttpHelper.OnDownloadListener {
private final Context mContext;
private final String mFileName;
private final NotificationManager mNotifyManager;
public UpdateListener(Context context, String fileName) {
mContext = context;
mFileName = fileName;
mNotifyManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
public void onDownloadStartConnect() {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(mContext.getString(R.string.start_update))
.setProgress(0, 0, true)
.setOngoing(true)
.setTicker(mContext.getString(R.string.start_update));
mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build());
}
@Override
public void onDownloadStartDownload(int totalSize) {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(mContext.getString(R.string.download_update))
.setContentText(String.format("%.2f / %.2f KB", 0/1024.0f, totalSize/1024.0f))
.setProgress(totalSize, 0, false)
.setOngoing(true);
mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build());
}
@Override
public void onDownloadStatusUpdate(
int downloadSize, int totalSize) {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(mContext.getString(R.string.download_update))
.setContentText(String.format("%.2f / %.2f KB", downloadSize/1024.0f, totalSize/1024.0f))
.setProgress(totalSize, downloadSize, false)
.setOngoing(true);
mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build());
}
@Override
public void onDownloadOver(int status, String eMsg) {
if (status == HttpHelper.DOWNLOAD_OK_CODE) {
mNotifyManager.cancel(NotificationId.UPDATE_ID);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(Config.getDownloadPath(), mFileName)),
"application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
} else {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext)
.setSmallIcon(android.R.drawable.stat_sys_warning)
.setContentTitle(mContext.getString(R.string.update_failed))
.setContentText(eMsg)
.setOngoing(false);
mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build());
}
setEnabled(true);
}
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.app;
import android.view.View;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.util.Ui;
import com.hippo.ehviewer.util.ViewUtils;
import com.hippo.ehviewer.widget.FileExplorerView;
public class DirSelectDialog extends MaterialAlertDialog {
private final FileExplorerView mFileExplorerView;
protected DirSelectDialog(Builder builder, FileExplorerView fileExplorerView) {
super(builder);
mFileExplorerView = fileExplorerView;
}
public static DirSelectDialog create(Builder builder, String path) {
View view = ViewUtils.inflateDialogView(R.layout.dir_selection, builder.mDarkTheme);
final FileExplorerView fileExplorerView = (FileExplorerView) view.findViewById(R.id.file_list);
final TextView warningTextView = (TextView) view.findViewById(R.id.warning);
final TextView pathTextView = (TextView) view.findViewById(R.id.path);
if (path != null)
fileExplorerView.setPath(path);
fileExplorerView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
fileExplorerView.onItemClick(parent, view, position, id);
pathTextView.setText(fileExplorerView.getCurPath());
if (fileExplorerView.canWrite())
warningTextView.setVisibility(View.GONE);
else
warningTextView.setVisibility(View.VISIBLE);
}
});
pathTextView.setText(fileExplorerView.getCurPath());
if (fileExplorerView.canWrite())
warningTextView.setVisibility(View.GONE);
else
warningTextView.setVisibility(View.VISIBLE);
builder.setView(view, false,
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, Ui.dp2pix(360)));
return new DirSelectDialog(builder, fileExplorerView);
}
public String getCurrentPath() {
return mFileExplorerView.getCurPath();
}
public void refresh() {
mFileExplorerView.refresh();
}
public boolean canWrite() {
return mFileExplorerView.canWrite();
}
}
@@ -0,0 +1,504 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.app;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.util.ViewUtils;
public class MaterialAlertDialog extends AlertDialog implements
View.OnClickListener, AdapterView.OnItemClickListener {
public final static int ACTION = 0x1;
public final static int POSITIVE = 0x2;
public final static int NEGATIVE = 0x4;
public final static int NEUTRAL = 0x8;
private final Context mContext;
private final Builder mBuilder;
private View mRootView;
private TextView mTitle;
private Button mActionButton;
private Button mPositiveButton;
private Button mNegativeButton;
private Button mNeutralButton;
protected MaterialAlertDialog(Builder builder) {
super(builder.mContext);
mContext = builder.mContext;
mBuilder = builder;
init();
}
@SuppressLint("InflateParams")
private void init() {
LayoutInflater inflater = LayoutInflater.from(mContext);
mRootView = inflater.inflate(R.layout.alert_dialog_material, null);
// Check in scroll view or not
boolean inScrollView;
if (mBuilder.mCustomView != null)
inScrollView = mBuilder.mInScrollView;
else if (mBuilder.mAdapter != null)
inScrollView = false;
else
inScrollView = true;
// Get widget
LinearLayout topPanel;
TextView message = null;
ListView list = null;
if (inScrollView) {
mRootView.findViewById(R.id.top_panel_noscroll).setVisibility(
View.GONE);
mTitle = (TextView) mRootView.findViewById(R.id.title);
topPanel = (LinearLayout) mRootView.findViewById(R.id.top_panel);
message = (TextView) mRootView.findViewById(R.id.message);
} else {
mRootView.findViewById(R.id.scroll_view).setVisibility(View.GONE);
mTitle = (TextView) mRootView.findViewById(R.id.title2);
topPanel = (LinearLayout) mRootView
.findViewById(R.id.top_panel_noscroll);
list = (ListView) mRootView.findViewById(R.id.list);
}
// Title
if (mBuilder.mTitle == null)
mTitle.setVisibility(View.GONE);
else
mTitle.setText(mBuilder.mTitle);
// Content
if (mBuilder.mCustomView != null) {
// CustomView
if (message != null)
message.setVisibility(View.GONE);
if (list != null)
list.setVisibility(View.GONE);
LinearLayout.LayoutParams lp = mBuilder.mCustomLp;
if (lp == null)
lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
mBuilder.mCustomView.setId(R.id.custom);
topPanel.addView(mBuilder.mCustomView, lp);
} else if (mBuilder.mAdapter != null) {
// List
if (message != null)
message.setVisibility(View.GONE);
list.setAdapter(mBuilder.mAdapter);
list.setOnItemClickListener(this);
if (mBuilder.mIsSingleChoice) {
list.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
if (mBuilder.mCheckedItem > -1) {
list.setItemChecked(mBuilder.mCheckedItem, true);
list.setSelection(mBuilder.mCheckedItem);
}
}
} else {
// Message
if (list != null)
list.setVisibility(View.GONE);
if (mBuilder.mAutoLinkMask != -1)
message.setAutoLinkMask(mBuilder.mAutoLinkMask);
message.setText(mBuilder.mMessage);
}
// Buttons
View buttonPanel = mRootView.findViewById(R.id.button_panel);
View nonActionPanel = mRootView.findViewById(R.id.non_action_panel);
mActionButton = (Button) mRootView.findViewById(R.id.button_action);
mPositiveButton = (Button) mRootView.findViewById(R.id.button_positive);
mNegativeButton = (Button) mRootView.findViewById(R.id.button_negative);
mNeutralButton = (Button) mRootView.findViewById(R.id.button_neutral);
boolean hasAction = mBuilder.mActionButtonText != null;
boolean hasNonAction = mBuilder.mPositiveButtonText != null
|| mBuilder.mNegativeButtonText != null
|| mBuilder.mNeutralButtonText != null;
boolean hasButton = hasAction || hasNonAction;
if (hasButton) {
if (hasAction) {
mActionButton.setText(mBuilder.mActionButtonText);
mActionButton.setOnClickListener(this);
} else {
mActionButton.setVisibility(View.GONE);
}
if (hasNonAction) {
// Positive
if (mBuilder.mPositiveButtonText != null) {
mPositiveButton.setText(mBuilder.mPositiveButtonText);
mPositiveButton.setOnClickListener(this);
} else {
mPositiveButton.setVisibility(View.GONE);
}
// Negative
if (mBuilder.mNegativeButtonText != null) {
mNegativeButton.setText(mBuilder.mNegativeButtonText);
mNegativeButton.setOnClickListener(this);
} else {
mNegativeButton.setVisibility(View.GONE);
}
// Neutral
if (mBuilder.mNeutralButtonText != null) {
mNeutralButton.setText(mBuilder.mNeutralButtonText);
mNeutralButton.setOnClickListener(this);
} else {
mNeutralButton.setVisibility(View.GONE);
}
} else {
nonActionPanel.setVisibility(View.GONE);
}
} else {
buttonPanel.setVisibility(View.GONE);
}
setView(mRootView);
setCancelable(mBuilder.mCancelable);
if (mBuilder.mOnCancelListener != null)
setOnCancelListener(mBuilder.mOnCancelListener);
if (mBuilder.mOnDismissListener != null)
setOnDismissListener(mBuilder.mOnDismissListener);
}
@Override
public void setTitle(CharSequence title) {
mTitle.setText(title);
}
@Override
public void setTitle(int resId) {
setTitle(getContext().getString(resId));
}
public void setView(View view, boolean inScrollView) {
setView(view, inScrollView, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
}
public void setView(View view, boolean inScrollView,
LinearLayout.LayoutParams lp) {
mRootView.findViewById(inScrollView ? R.id.top_panel_noscroll : R.id.scroll_view).setVisibility(View.GONE);
mRootView.findViewById(inScrollView ? R.id.scroll_view : R.id.top_panel_noscroll).setVisibility(View.VISIBLE);
mRootView.findViewById(R.id.message).setVisibility(View.GONE);
mRootView.findViewById(R.id.list).setVisibility(View.GONE);
LinearLayout topPanel = (LinearLayout) mRootView.findViewById(inScrollView ? R.id.top_panel : R.id.top_panel_noscroll);
topPanel.addView(view, lp);
// Title
TextView oldTitle = mTitle;
mTitle = (TextView) mRootView.findViewById(inScrollView ? R.id.title : R.id.title2);
if (mTitle != oldTitle && oldTitle != null)
mTitle.setText(oldTitle.getText());
}
@Override
public void onClick(View v) {
boolean close = true;
if (mBuilder.mButtonListener != null) {
if (v == mActionButton)
close = mBuilder.mButtonListener.onClick(this, ACTION);
else if (v == mPositiveButton)
close = mBuilder.mButtonListener.onClick(this, POSITIVE);
else if (v == mNegativeButton)
close = mBuilder.mButtonListener.onClick(this, NEGATIVE);
else if (v == mNeutralButton)
close = mBuilder.mButtonListener.onClick(this, NEUTRAL);
}
if (close)
dismiss();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
boolean close = false;
if (mBuilder.mOnClickListener != null)
close = mBuilder.mOnClickListener.onClick(this, position);
if (close)
dismiss();
}
public static interface OnClickListener {
public boolean onClick(MaterialAlertDialog dialog, int which);
}
public static class Builder {
protected Context mContext;
protected boolean mDarkTheme = false;
protected CharSequence mTitle;
protected CharSequence mMessage;
protected CharSequence mActionButtonText;
protected CharSequence mPositiveButtonText;
protected CharSequence mNegativeButtonText;
protected CharSequence mNeutralButtonText;
protected OnClickListener mButtonListener;
protected View mCustomView;
protected LinearLayout.LayoutParams mCustomLp;
protected boolean mInScrollView = true;
protected boolean mCancelable = true;
protected int mAutoLinkMask = -1;
protected OnCancelListener mOnCancelListener;
protected ListAdapter mAdapter;
protected OnClickListener mOnClickListener;
protected int mCheckedItem;
protected boolean mIsSingleChoice;
protected DialogInterface.OnDismissListener mOnDismissListener;
public Builder(Context context) {
mContext = context;
}
public Builder setDrakTheme(boolean darkTheme) {
mDarkTheme = darkTheme;
return this;
}
public Builder setTitle(CharSequence title) {
mTitle = title;
return this;
}
public Builder setTitle(int resId) {
return setTitle(mContext.getString(resId));
}
public Builder setMessage(CharSequence message) {
mMessage = message;
return this;
}
public Builder setMessage(int resId) {
return setMessage(mContext.getString(resId));
}
public Builder setActionButton(CharSequence text) {
mActionButtonText = text;
return this;
}
public Builder setActionButton(int resId) {
return setActionButton(mContext.getString(resId));
}
public Builder setPositiveButton(CharSequence text) {
mPositiveButtonText = text;
return this;
}
public Builder setPositiveButton(int resId) {
return setPositiveButton(mContext.getString(resId));
}
public Builder setNegativeButton(CharSequence text) {
mNegativeButtonText = text;
return this;
}
public Builder setNegativeButton(int resId) {
return setNegativeButton(mContext.getString(resId));
}
public Builder setNeutralButton(CharSequence text) {
mNeutralButtonText = text;
return this;
}
public Builder setNeutralButton(int resId) {
return setNeutralButton(mContext.getString(resId));
}
public Builder setDefaultButton(int b) {
if ((b & POSITIVE) == POSITIVE)
mPositiveButtonText = mContext.getString(android.R.string.ok);
if ((b & NEGATIVE) == NEGATIVE)
mNegativeButtonText = mContext.getString(android.R.string.cancel);
return this;
}
public Builder setButtonListener(OnClickListener l) {
mButtonListener = l;
return this;
}
/**
* Custom view id will be R.id.custom
*
* @param view
* @param inScrollView
* @return
*/
public Builder setView(View view, boolean inScrollView) {
mCustomView = view;
mInScrollView = inScrollView;
return this;
}
/**
* Custom view id will be R.id.custom
*
* @param view
* @param inScrollView
* @param lp
* @return
*/
public Builder setView(View view, boolean inScrollView,
LinearLayout.LayoutParams lp) {
mCustomView = view;
mInScrollView = inScrollView;
mCustomLp = lp;
return this;
}
public Builder setView(int resId, boolean inScrollView, ViewHolder vh) {
mCustomView = ViewUtils.inflateDialogView(resId, mDarkTheme);
mInScrollView = inScrollView;
if (vh != null)
vh.setView(mCustomView);
return this;
}
public Builder setCancelable(boolean cancelable) {
mCancelable = cancelable;
return this;
}
public Builder setOnCancelListener(OnCancelListener onCancelListener) {
mOnCancelListener = onCancelListener;
return this;
}
public Builder setMessageAutoLink(int mask) {
mAutoLinkMask = mask;
return this;
}
public Builder setItems(int itemsId, final OnClickListener listener) {
mAdapter = new ArrayAdapter<CharSequence>(
mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper,
R.layout.select_dialog_item, android.R.id.text1, mContext
.getResources().getTextArray(itemsId));
mOnClickListener = listener;
mIsSingleChoice = false;
return this;
}
public Builder setItems(CharSequence[] items,
final OnClickListener listener) {
mAdapter = new ArrayAdapter<CharSequence>(
mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper,
R.layout.select_dialog_item, android.R.id.text1, items);
mOnClickListener = listener;
mIsSingleChoice = false;
return this;
}
public Builder setAdapter(final ListAdapter adapter,
final OnClickListener listener) {
mAdapter = adapter;
mOnClickListener = listener;
mIsSingleChoice = false;
return this;
}
public Builder setSingleChoiceItems(int itemsId, int checkedItem,
final OnClickListener listener) {
mAdapter = new ArrayAdapter<CharSequence>(
mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper,
R.layout.select_dialog_singlechoice, android.R.id.text1,
mContext.getResources().getTextArray(itemsId));
mOnClickListener = listener;
mCheckedItem = checkedItem;
mIsSingleChoice = true;
return this;
}
public Builder setSingleChoiceItems(CharSequence[] items,
int checkedItem, final OnClickListener listener) {
mAdapter = new ArrayAdapter<CharSequence>(
mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper,
R.layout.select_dialog_singlechoice, android.R.id.text1,
items);
mOnClickListener = listener;
mCheckedItem = checkedItem;
mIsSingleChoice = true;
return this;
}
public Builder setSingleChoiceItems(ListAdapter adapter,
int checkedItem, final OnClickListener listener) {
mAdapter = adapter;
mOnClickListener = listener;
mCheckedItem = checkedItem;
mIsSingleChoice = true;
return this;
}
public Builder setOnDismissListener(DialogInterface.OnDismissListener l) {
mOnDismissListener = l;
return this;
}
public MaterialAlertDialog create() {
mContext = new ContextThemeWrapper(mContext, mDarkTheme ?
R.style.AppTheme_Dark_Dialog : R.style.AppTheme_Dialog);
return new MaterialAlertDialog(this);
}
public MaterialAlertDialog show() {
MaterialAlertDialog dialog = create();
dialog.show();
return dialog;
}
}
public static class ViewHolder {
private View mView;
private void setView(View v) {
mView = v;
}
public View getView() {
return mView;
}
}
}
@@ -0,0 +1,289 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.app;
import java.text.NumberFormat;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.hippo.ehviewer.R;
public class MaterialProgressDialog extends MaterialAlertDialog {
/** Creates a ProgressDialog with a circular, spinning progress
* bar. This is the default.
*/
public static final int STYLE_SPINNER = 0;
/** Creates a ProgressDialog with a horizontal progress bar.
*/
public static final int STYLE_HORIZONTAL = 1;
private ProgressBar mProgress;
private TextView mMessageView;
private int mProgressStyle = STYLE_HORIZONTAL;
private TextView mProgressNumber;
private String mProgressNumberFormat;
private TextView mProgressPercent;
private NumberFormat mProgressPercentFormat;
private int mMax;
private int mProgressVal;
private int mSecondaryProgressVal;
private boolean mIndeterminate;
private boolean mHasStarted;
private Handler mViewUpdateHandler;
@SuppressLint({ "InflateParams", "HandlerLeak" })
protected MaterialProgressDialog(Builder builder) {
super(builder);
initFormats();
LayoutInflater inflater = LayoutInflater.from(builder.mContext);
if (mProgressStyle == STYLE_HORIZONTAL) {
/* Use a separate handler to update the text views as they
* must be updated on the same thread that created them.
*/
mViewUpdateHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
/* Update the number and percent */
int progress = mProgress.getProgress();
int max = mProgress.getMax();
if (mProgressNumberFormat != null) {
String format = mProgressNumberFormat;
mProgressNumber.setText(String.format(format, progress, max));
} else {
mProgressNumber.setText("");
}
if (mProgressPercentFormat != null) {
double percent = (double) progress / (double) max;
SpannableString tmp = new SpannableString(mProgressPercentFormat.format(percent));
tmp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
0, tmp.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mProgressPercent.setText(tmp);
} else {
mProgressPercent.setText("");
}
}
};
View view = inflater.inflate(R.layout.alert_dialog_progress, null);
mProgress = (ProgressBar) view.findViewById(R.id.progress);
mProgressNumber = (TextView) view.findViewById(R.id.progress_number);
mProgressPercent = (TextView) view.findViewById(R.id.progress_percent);
setView(view, false);
} else {
View view = inflater.inflate(R.layout.progress_dialog, null);
mProgress = (ProgressBar) view.findViewById(R.id.progress);
mMessageView = (TextView) view.findViewById(R.id.message);
setView(view, false);
}
}
private void initFormats() {
mProgressNumberFormat = "%1d/%2d";
mProgressPercentFormat = NumberFormat.getPercentInstance();
mProgressPercentFormat.setMaximumFractionDigits(0);
}
public static MaterialProgressDialog create(Context context, CharSequence title) {
return create(context, title, false, null);
}
public static MaterialProgressDialog create(Context context, CharSequence title,
boolean cancelable) {
return create(context, title, cancelable, null);
}
public static MaterialProgressDialog create(Context context, CharSequence title,
boolean cancelable, OnCancelListener cancelListener) {
MaterialProgressDialog.Builder builder = new MaterialProgressDialog.Builder(context);
builder.setTitle(title);
builder.setCancelable(cancelable);
builder.setOnCancelListener(cancelListener);
return new MaterialProgressDialog(builder);
}
@Override
public void onStart() {
super.onStart();
mHasStarted = true;
}
@Override
protected void onStop() {
super.onStop();
mHasStarted = false;
}
public void setProgress(int value) {
if (mHasStarted) {
mProgress.setProgress(value);
onProgressChanged();
} else {
mProgressVal = value;
}
}
public void setSecondaryProgress(int secondaryProgress) {
if (mProgress != null) {
mProgress.setSecondaryProgress(secondaryProgress);
onProgressChanged();
} else {
mSecondaryProgressVal = secondaryProgress;
}
}
public int getProgress() {
if (mProgress != null) {
return mProgress.getProgress();
}
return mProgressVal;
}
public int getSecondaryProgress() {
if (mProgress != null) {
return mProgress.getSecondaryProgress();
}
return mSecondaryProgressVal;
}
public int getMax() {
if (mProgress != null) {
return mProgress.getMax();
}
return mMax;
}
public void setMax(int max) {
if (mProgress != null) {
mProgress.setMax(max);
onProgressChanged();
} else {
mMax = max;
}
}
public void incrementProgressBy(int diff) {
if (mProgress != null) {
mProgress.incrementProgressBy(diff);
onProgressChanged();
}
}
public void incrementSecondaryProgressBy(int diff) {
if (mProgress != null) {
mProgress.incrementSecondaryProgressBy(diff);
onProgressChanged();
}
}
public void setProgressDrawable(Drawable d) {
if (mProgress != null) {
mProgress.setProgressDrawable(d);
}
}
public void setIndeterminateDrawable(Drawable d) {
if (mProgress != null) {
mProgress.setIndeterminateDrawable(d);
}
}
public void setIndeterminate(boolean indeterminate) {
if (mProgress != null) {
mProgress.setIndeterminate(indeterminate);
}
}
public boolean isIndeterminate() {
if (mProgress != null) {
return mProgress.isIndeterminate();
}
return mIndeterminate;
}
@Override
public void setMessage(CharSequence message) {
if (mProgress != null) {
if (mProgressStyle == STYLE_HORIZONTAL) {
super.setMessage(message);
} else {
mMessageView.setText(message);
}
}
}
public void setProgressStyle(int style) {
mProgressStyle = style;
}
/**
* Change the format of the small text showing current and maximum units
* of progress. The default is "%1d/%2d".
* Should not be called during the number is progressing.
* @param format A string passed to {@link String#format String.format()};
* use "%1d" for the current number and "%2d" for the maximum. If null,
* nothing will be shown.
*/
public void setProgressNumberFormat(String format) {
mProgressNumberFormat = format;
onProgressChanged();
}
/**
* Change the format of the small text showing the percentage of progress.
* The default is
* {@link NumberFormat#getPercentInstance() NumberFormat.getPercentageInstnace().}
* Should not be called during the number is progressing.
* @param format An instance of a {@link NumberFormat} to generate the
* percentage text. If null, nothing will be shown.
*/
public void setProgressPercentFormat(NumberFormat format) {
mProgressPercentFormat = format;
onProgressChanged();
}
private void onProgressChanged() {
if (mProgressStyle == STYLE_HORIZONTAL) {
if (mViewUpdateHandler != null && !mViewUpdateHandler.hasMessages(0)) {
mViewUpdateHandler.sendEmptyMessage(0);
}
}
}
}
@@ -0,0 +1,969 @@
/*
* Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
* License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
* or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package com.hippo.ehviewer.cache;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
****************************************************************************** Taken from the JB source code, can be found in:
* libcore/luni/src/main/java/libcore/io/DiskLruCache.java or direct link:
* https:
* //android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/
* main/java/libcore/io/DiskLruCache.java A cache that uses a bounded amount of
* space on a filesystem. Each cache entry has a string key and a fixed number
* of values. Values are byte sequences, accessible as streams or files. Each
* value must be between {@code 0} and {@code Integer.MAX_VALUE} bytes in
* length.
* <p>
* The cache stores its data in a directory on the filesystem. This directory
* must be exclusive to the cache; the cache may delete or overwrite files from
* its directory. It is an error for multiple processes to use the same cache
* directory at the same time.
* <p>
* This cache limits the number of bytes that it will store on the filesystem.
* When the number of stored bytes exceeds the limit, the cache will remove
* entries in the background until the limit is satisfied. The limit is not
* strict: the cache may temporarily exceed it while waiting for files to be
* deleted. The limit does not include filesystem overhead or the cache journal
* so space-sensitive applications should set a conservative limit.
* <p>
* Clients call {@link #edit} to create or update the values of an entry. An
* entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null.
* <ul>
* <li>When an entry is being <strong>created</strong> it is necessary to supply
* a full set of values; the empty value should be used as a placeholder if
* necessary.
* <li>When an entry is being <strong>edited</strong>, it is not necessary to
* supply data for every value; values default to their previous value.
* </ul>
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
* of values as they were before or after the commit, but never a mix of values.
* <p>
* Clients call {@link #get} to read a snapshot of an entry. The read will
* observe the value at the time that {@link #get} was called. Updates and
* removals after the call do not impact ongoing reads.
* <p>
* This class is tolerant of some I/O errors. If files are missing from the
* filesystem, the corresponding entries will be dropped from the cache. If an
* error occurs while writing a cache value, the edit will fail silently.
* Callers should handle other problems by catching {@code IOException} and
* responding appropriately.
*/
public final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TMP = "journal.tmp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final int IO_BUFFER_SIZE = 8 * 1024;
/*
* This cache uses a journal file named "journal". A typical journal file
* looks like this: libcore.io.DiskLruCache 1 100 2 CLEAN
* 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 DIRTY
* 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52
* 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY
* 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a
* 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ
* 3400330d1dfc7f3f7f4b8d4d803dfcf6 The first five lines of the journal form
* its header. They are the constant string "libcore.io.DiskLruCache", the
* disk cache's version, the application's version, the value count, and a
* blank line. Each of the subsequent lines in the file is a record of the
* state of a cache entry. Each line contains space-separated values: a
* state, a key, and optional state-specific values. o DIRTY lines track
* that an entry is actively being created or updated. Every successful
* DIRTY action should be followed by a CLEAN or REMOVE action. DIRTY lines
* without a matching CLEAN or REMOVE indicate that temporary files may need
* to be deleted. o CLEAN lines track a cache entry that has been
* successfully published and may be read. A publish line is followed by the
* lengths of each of its values. o READ lines track accesses for LRU. o
* REMOVE lines track entries that have been deleted. The journal file is
* appended to as cache operations occur. The journal may occasionally be
* compacted by dropping redundant lines. A temporary file named
* "journal.tmp" will be used during compaction; that file should be deleted
* if it exists when the cache is opened.
*/
private final File directory;
private final File journalFile;
private final File journalFileTmp;
private final int appVersion;
private final long maxSize;
private final int valueCount;
private long size = 0;
private Writer journalWriter;
private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0,
0.75f, true);
private int redundantOpCount;
/**
* To differentiate between old and current snapshots, each entry is given a
* sequence number each time an edit is committed. A snapshot is stale if
* its sequence number is not equal to its entry's sequence number.
*/
private long nextSequenceNumber = 0;
/* From java.util.Arrays */
@SuppressWarnings("unchecked")
private static <T> T[] copyOfRange(final T[] original, final int start, final int end) {
final int originalLength = original.length; // For exception priority
// compatibility.
if (start > end) {
throw new IllegalArgumentException();
}
if (start < 0 || start > originalLength) {
throw new ArrayIndexOutOfBoundsException();
}
final int resultLength = end - start;
final int copyLength = Math.min(resultLength, originalLength - start);
final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(),
resultLength);
System.arraycopy(original, start, result, 0, copyLength);
return result;
}
/**
* Returns the remainder of 'reader' as a string, closing it when done.
*/
public static String readFully(final Reader reader) throws IOException {
try {
final StringWriter writer = new StringWriter();
final char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1) {
writer.write(buffer, 0, count);
}
return writer.toString();
} finally {
reader.close();
}
}
/**
* Returns the ASCII characters up to but not including the next "\r\n", or
* "\n".
*
* @throws java.io.EOFException if the stream is exhausted before the next
* newline character.
*/
public static String readAsciiLine(final InputStream in) throws IOException {
// TODO: support UTF-8 here instead
final StringBuilder result = new StringBuilder(80);
while (true) {
final int c = in.read();
if (c == -1) {
throw new EOFException();
} else if (c == '\n') {
break;
}
result.append((char)c);
}
final int length = result.length();
if (length > 0 && result.charAt(length - 1) == '\r') {
result.setLength(length - 1);
}
return result.toString();
}
/**
* Closes 'closeable', ignoring any checked exceptions. Does nothing if
* 'closeable' is null.
*/
public static void closeQuietly(final Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (final RuntimeException rethrown) {
throw rethrown;
} catch (final Exception ignored) {
}
}
}
/**
* Recursively delete everything in {@code dir}.
*/
// TODO: this should specify paths as Strings rather than as Files
public static void deleteContents(final File dir) throws IOException {
final File[] files = dir.listFiles();
if (files == null) {
throw new IllegalArgumentException("not a directory: " + dir);
}
for (final File file : files) {
if (file.isDirectory()) {
deleteContents(file);
}
if (!file.delete()) {
throw new IOException("failed to delete file: " + file);
}
}
}
/** This cache uses a single background thread to evict entries. */
private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Callable<Void> cleanupCallable = new Callable<Void>() {
@Override
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // closed
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
private DiskLruCache(final File directory, final int appVersion, final int valueCount,
final long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
journalFile = new File(directory, JOURNAL_FILE);
journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* @param appVersion
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws IOException if reading or writing the cache directory fails
*/
public static DiskLruCache open(final File directory, final int appVersion,
final int valueCount, final long maxSize) throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (final IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
private void readJournal() throws IOException {
final InputStream in = new BufferedInputStream(new FileInputStream(journalFile),
IO_BUFFER_SIZE);
try {
final String magic = readAsciiLine(in);
final String version = readAsciiLine(in);
final String appVersionString = readAsciiLine(in);
final String valueCountString = readAsciiLine(in);
final String blank = readAsciiLine(in);
if (!MAGIC.equals(magic) || !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)) {
throw new IOException("unexpected journal header: [" + magic + ", " + version
+ ", " + valueCountString + ", " + blank + "]");
}
while (true) {
try {
readJournalLine(readAsciiLine(in));
} catch (final EOFException endOfJournal) {
break;
}
}
} finally {
closeQuietly(in);
}
}
private void readJournalLine(final String line) throws IOException {
final String[] parts = line.split(" ");
if (parts.length < 2) {
throw new IOException("unexpected journal line: " + line);
}
final String key = parts[1];
if (parts[0].equals(REMOVE) && parts.length == 2) {
lruEntries.remove(key);
return;
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(copyOfRange(parts, 2, parts.length));
} else if (parts[0].equals(DIRTY) && parts.length == 2) {
entry.currentEditor = new Editor(entry);
} else if (parts[0].equals(READ) && parts.length == 2) {
// this work was already done by calling lruEntries.get()
} else {
throw new IOException("unexpected journal line: " + line);
}
}
/**
* Computes the initial size and collects garbage as a part of opening the
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
*/
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (final Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
final Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
/**
* Creates a new journal that omits redundant information. This replaces the
* current journal if it exists.
*/
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
final Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
writer.write(MAGIC);
writer.write("\n");
writer.write(VERSION_1);
writer.write("\n");
writer.write(Integer.toString(appVersion));
writer.write("\n");
writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n");
for (final Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
}
writer.close();
journalFileTmp.renameTo(journalFile);
journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
}
private static void deleteIfExists(final File file) throws IOException {
// try {
// Libcore.os.remove(file.getPath());
// } catch (ErrnoException errnoException) {
// if (errnoException.errno != OsConstants.ENOENT) {
// throw errnoException.rethrowAsIOException();
// }
// }
if (file.exists() && !file.delete()) {
throw new IOException();
}
}
/**
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
* exist is not currently readable. If a value is returned, it is moved to
* the head of the LRU queue.
*/
public synchronized Snapshot get(final String key) throws IOException {
checkNotClosed();
validateKey(key);
final Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
/*
* Open all streams eagerly to guarantee that we see a single published
* snapshot. If we opened streams lazily then the streams could come
* from different edits.
*/
final InputStream[] ins = new InputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (final FileNotFoundException e) {
// a file must have been deleted manually!
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins);
}
/**
* Returns an editor for the entry named {@code key}, or null if another
* edit is in progress.
*/
public Editor edit(final String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(final String key, final long expectedSequenceNumber)
throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
&& (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
return null; // snapshot is stale
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // another edit is in progress
}
final Editor editor = new Editor(entry);
entry.currentEditor = editor;
// flush the journal before creating files to prevent file leaks
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
/**
* Returns the directory where this cache stores its data.
*/
public File getDirectory() {
return directory;
}
/**
* Returns the maximum number of bytes that this cache should use to store
* its data.
*/
public long maxSize() {
return maxSize;
}
/**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
* deletion is pending.
*/
public synchronized long size() {
return size;
}
private synchronized void completeEdit(final Editor editor, final boolean success)
throws IOException {
final Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// if this edit is creating the entry for the first time, every index
// must have a value
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
throw new IllegalStateException("edit didn't create file " + i);
}
}
}
for (int i = 0; i < valueCount; i++) {
final File dirty = entry.getDirtyFile(i);
if (success) {
if (dirty.exists()) {
final File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
final long oldLength = entry.lengths[i];
final long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
/**
* We only rebuild the journal when it will halve the size of the journal
* and eliminate at least 2000 ops.
*/
private boolean journalRebuildRequired() {
final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
&& redundantOpCount >= lruEntries.size();
}
/**
* Drops the entry for {@code key} if it exists and can be removed. Entries
* actively being edited cannot be removed.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(final String key) throws IOException {
checkNotClosed();
validateKey(key);
final Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
for (int i = 0; i < valueCount; i++) {
final File file = entry.getCleanFile(i);
if (!file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(REMOVE + ' ' + key + '\n');
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
/**
* Returns true if this cache has been closed.
*/
public boolean isClosed() {
return journalWriter == null;
}
private void checkNotClosed() {
if (journalWriter == null) {
throw new IllegalStateException("cache is closed");
}
}
/**
* Force buffered operations to the filesystem.
*/
public synchronized void flush() throws IOException {
checkNotClosed();
trimToSize();
journalWriter.flush();
}
/**
* Closes this cache. Stored values will remain on the filesystem.
*/
@Override
public synchronized void close() throws IOException {
if (journalWriter == null) {
return; // already closed
}
for (final Entry entry : new ArrayList<Entry>(lruEntries.values())) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
private void trimToSize() throws IOException {
while (size > maxSize) {
// Map.Entry<String, Entry> toEvict = lruEntries.eldest();
final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
/**
* Closes the cache and deletes all of its stored values. This will delete
* all files in the cache directory including files that weren't created by
* the cache.
*/
public void delete() throws IOException {
close();
deleteContents(directory);
}
private void validateKey(final String key) {
if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key
+ "\"");
}
}
private static String inputStreamToString(final InputStream in) throws IOException {
return readFully(new InputStreamReader(in, UTF_8));
}
/**
* A snapshot of the values for an entry.
*/
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
private Snapshot(final String key, final long sequenceNumber, final InputStream[] ins) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
}
/**
* Returns an editor for this snapshot's entry, or null if either the
* entry has changed since this snapshot was created or if another edit
* is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
/**
* Returns the unbuffered stream with the value for {@code index}.
*/
public InputStream getInputStream(final int index) {
return ins[index];
}
/**
* Returns the string value for {@code index}.
*/
public String getString(final int index) throws IOException {
return inputStreamToString(getInputStream(index));
}
@Override
public void close() {
for (final InputStream in : ins) {
closeQuietly(in);
}
}
}
/**
* Edits the values for an entry.
*/
public final class Editor {
private final Entry entry;
private boolean hasErrors;
private Editor(final Entry entry) {
this.entry = entry;
}
/**
* Returns an unbuffered input stream to read the last committed value,
* or null if no value has been committed.
*/
public InputStream newInputStream(final int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
return null;
}
return new FileInputStream(entry.getCleanFile(index));
}
}
/**
* Returns the last committed value as a string, or null if no value has
* been committed.
*/
public String getString(final int index) throws IOException {
final InputStream in = newInputStream(index);
return in != null ? inputStreamToString(in) : null;
}
/**
* Returns a new unbuffered output stream to write the value at
* {@code index}. If the underlying output stream encounters errors when
* writing to the filesystem, this edit will be aborted when
* {@link #commit} is called. The returned output stream does not throw
* IOExceptions.
*/
public OutputStream newOutputStream(final int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
}
}
/**
* Sets the value at {@code index} to {@code value}.
*/
public void set(final int index, final String value) throws IOException {
Writer writer = null;
try {
writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
writer.write(value);
} finally {
closeQuietly(writer);
}
}
/**
* Commits this edit so it is visible to readers. This releases the edit
* lock so another edit may be started on the same key.
*/
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // the previous entry is stale
} else {
completeEdit(this, true);
}
}
/**
* Aborts this edit. This releases the edit lock so another edit may be
* started on the same key.
*/
public void abort() throws IOException {
completeEdit(this, false);
}
private class FaultHidingOutputStream extends FilterOutputStream {
private FaultHidingOutputStream(final OutputStream out) {
super(out);
}
@Override
public void write(final int oneByte) {
try {
out.write(oneByte);
} catch (final IOException e) {
hasErrors = true;
}
}
@Override
public void write(final byte[] buffer, final int offset, final int length) {
try {
out.write(buffer, offset, length);
} catch (final IOException e) {
hasErrors = true;
}
}
@Override
public void close() {
try {
out.close();
} catch (final IOException e) {
hasErrors = true;
}
}
@Override
public void flush() {
try {
out.flush();
} catch (final IOException e) {
hasErrors = true;
}
}
}
}
private final class Entry {
private final String key;
/** Lengths of this entry's files. */
private final long[] lengths;
/** True if this entry has ever been published */
private boolean readable;
/** The ongoing edit or null if this entry is not being edited. */
private Editor currentEditor;
/**
* The sequence number of the most recently committed edit to this
* entry.
*/
private long sequenceNumber;
private Entry(final String key) {
this.key = key;
lengths = new long[valueCount];
}
public String getLengths() throws IOException {
final StringBuilder result = new StringBuilder();
for (final long size : lengths) {
result.append(' ').append(size);
}
return result.toString();
}
/**
* Set lengths using decimal numbers like "10123".
*/
private void setLengths(final String[] strings) throws IOException {
if (strings.length != valueCount) {
throw invalidLengths(strings);
}
try {
for (int i = 0; i < strings.length; i++) {
lengths[i] = Long.parseLong(strings[i]);
}
} catch (final NumberFormatException e) {
throw invalidLengths(strings);
}
}
private IOException invalidLengths(final String[] strings) throws IOException {
throw new IOException("unexpected journal line: " + Arrays.toString(strings));
}
public File getCleanFile(final int i) {
return new File(directory, key + "." + i);
}
public File getDirtyFile(final int i) {
return new File(directory, key + "." + i + ".tmp");
}
}
}
@@ -0,0 +1,721 @@
/*
* Copyright (C) 2012 Andrew Neal 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.hippo.ehviewer.cache;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Looper;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import com.hippo.ehviewer.util.Utils;
/**
* This class holds the memory and disk bitmap caches.
*/
public final class ImageCache {
private static final String TAG = ImageCache.class.getSimpleName();
/**
* Default memory cache size as a percent of device memory class
*/
private static final float MEM_CACHE_DIVIDER = 0.25f;
/**
* Default disk cache size 10MB
*/
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
/**
* Compression settings when writing images to disk cache
*/
private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
/**
* Disk cache index to read from
*/
private static final int DISK_CACHE_INDEX = 0;
/**
* Image compression quality
*/
private static final int COMPRESS_QUALITY = 98;
/**
* LRU cache
*/
private MemoryCache mLruCache;
/**
* Disk LRU cache
*/
private DiskLruCache mDiskCache;
private static ImageCache sInstance;
/**
* Used to temporarily pause the disk cache while scrolling
*/
public boolean mPauseDiskAccess = false;
private final Object mPauseLock = new Object();
/**
* Constructor of <code>ImageCache</code>
*
* @param context The {@link Context} to use
*/
public ImageCache(final Context context) {
init(context);
}
/**
* Used to create a singleton of {@link ImageCache}
*
* @param context The {@link Context} to use
* @return A new instance of this class.
*/
public final static ImageCache getInstance(final Context context) {
if (sInstance == null) {
sInstance = new ImageCache(context.getApplicationContext());
}
return sInstance;
}
/**
* Initialize the cache, providing all parameters.
*
* @param context The {@link Context} to use
* @param cacheParams The cache parameters to initialize the cache
*/
private void init(final Context context) {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
// Initialize the disk cahe in a background thread
initDiskCache(context);
return null;
}
}, (Void[])null);
// Set up the memory cache
initLruCache(context);
}
/**
* Initializes the disk cache. Note that this includes disk access so this
* should not be executed on the main/UI thread. By default an ImageCache
* does not initialize the disk cache when it is created, instead you should
* call initDiskCache() to initialize it on a background thread.
*
* @param context The {@link Context} to use
*/
private synchronized void initDiskCache(final Context context) {
// Set up disk cache
if (mDiskCache == null || mDiskCache.isClosed()) {
File diskCacheDir = getDiskCacheDir(context, TAG);
if (diskCacheDir != null) {
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
} catch (final IOException e) {
diskCacheDir = null;
}
}
}
}
}
/**
* Sets up the Lru cache
*
* @param context The {@link Context} to use
*/
@SuppressLint("NewApi")
public void initLruCache(final Context context) {
final ActivityManager activityManager = (ActivityManager)context
.getSystemService(Context.ACTIVITY_SERVICE);
final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
* 1024 * 1024);
mLruCache = new MemoryCache(lruCacheSize);
// Release some memory as needed
context.registerComponentCallbacks(new ComponentCallbacks2() {
/**
* {@inheritDoc}
*/
@Override
public void onTrimMemory(final int level) {
if (level >= TRIM_MEMORY_MODERATE) {
evictAll();
} else if (level >= TRIM_MEMORY_BACKGROUND) {
mLruCache.trimToSize(mLruCache.size() / 2);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLowMemory() {
// Nothing to do
}
/**
* {@inheritDoc}
*/
@Override
public void onConfigurationChanged(final Configuration newConfig) {
// Nothing to do
}
});
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}
* , if not found a new one is created using the supplied params and saved
* to a {@link RetainFragment}
*
* @param activity The calling {@link FragmentActivity}
* @return An existing retained ImageCache object or a new one if one did
* not exist
*/
public static final ImageCache findOrCreateCache(final Activity activity) {
// Search for, or create an instance of the non-UI RetainFragment
final RetainFragment retainFragment = findOrCreateRetainFragment(
activity.getFragmentManager());
// See if we already have an ImageCache stored in RetainFragment
ImageCache cache = (ImageCache)retainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment
if (cache == null) {
cache = getInstance(activity);
retainFragment.setObject(cache);
}
return cache;
}
/**
* Locate an existing instance of this {@link Fragment} or if not found,
* create and add it using {@link FragmentManager}
*
* @param fm The {@link FragmentManager} to use
* @return The existing instance of the {@link Fragment} or the new instance
* if just created
*/
public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
// Check to see if we have retained the worker fragment
RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
// If not retained, we need to create and add it
if (retainFragment == null) {
retainFragment = new RetainFragment();
fm.beginTransaction().add(retainFragment, TAG).commit();
}
return retainFragment;
}
/**
* Adds a new image to the memory and disk caches
*
* @param data The key used to store the image
* @param bitmap The {@link Bitmap} to cache
*/
public void addBitmapToCache(final String data, final Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
addBitmapToMemCache(data, bitmap);
// Add to disk cache
if (mDiskCache != null) {
final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
editor.commit();
out.close();
flush();
}
} else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} finally {
try {
if (out != null) {
out.close();
out = null;
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} catch (final IllegalStateException e) {
Log.e(TAG, "addBitmapToCache - " + e);
}
}
}
}
/**
* Called to add a new image to the memory cache
*
* @param data The key identifier
* @param bitmap The {@link Bitmap} to cache
*/
public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
if (getBitmapFromMemCache(data) == null) {
mLruCache.put(data, bitmap);
}
}
/**
* Fetches a cached image from the memory cache
*
* @param data Unique identifier for which item to get
* @return The {@link Bitmap} if found in cache, null otherwise
*/
public final Bitmap getBitmapFromMemCache(final String data) {
if (data == null) {
return null;
}
if (mLruCache != null) {
final Bitmap lruBitmap = mLruCache.get(data);
if (lruBitmap != null) {
return lruBitmap;
}
}
return null;
}
/**
* Fetches a cached image from the disk cache
*
* @param data Unique identifier for which item to get
* @return The {@link Bitmap} if found in cache, null otherwise
*/
public final Bitmap getBitmapFromDiskCache(final String data) {
if (data == null) {
return null;
}
// Check in the memory cache here to avoid going to the disk cache less
// often
if (getBitmapFromMemCache(data) != null) {
return getBitmapFromMemCache(data);
}
waitUntilUnpaused();
final String key = hashKeyForDisk(data);
if (mDiskCache != null) {
InputStream inputStream = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
if (snapshot != null) {
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
return bitmap;
}
}
}
} catch (final IOException e) {
Log.e(TAG, "getBitmapFromDiskCache - " + e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (final IOException e) {
}
}
}
return null;
}
public static final int STATE_NONE = 0;
public static final int STATE_FROM_MEMORY = 1;
public static final int STATE_FROM_DISK = 2;
/**
* Tries to return a cached image from memory cache before fetching from the
* disk cache
*
* @param data Unique identifier for which item to get
* @return The {@link Bitmap} if found in cache, null otherwise
*/
public final Bitmap getCachedBitmap(final String data, int[] state) {
if (data == null) {
return null;
}
Bitmap cachedImage = getBitmapFromMemCache(data);
if (cachedImage == null) {
cachedImage = getBitmapFromDiskCache(data);
if (cachedImage != null) {
addBitmapToMemCache(data, cachedImage);
if (state != null)
state[0] = STATE_FROM_DISK;
} else {
if (state != null)
state[0] = STATE_NONE;
}
} else {
if (state != null)
state[0] = STATE_FROM_MEMORY;
}
return cachedImage;
}
/**
* flush() is called to synchronize up other methods that are accessing the
* cache first
*/
public void flush() {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
if (mDiskCache != null) {
try {
if (!mDiskCache.isClosed()) {
mDiskCache.flush();
}
} catch (final IOException e) {
Log.e(TAG, "flush - " + e);
}
}
return null;
}
}, (Void[])null);
}
/**
* Clears the disk and memory caches
*/
public void clearCaches() {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
// Clear the disk cache
try {
if (mDiskCache != null) {
mDiskCache.delete();
mDiskCache = null;
}
} catch (final IOException e) {
Log.e(TAG, "clearCaches - " + e);
}
// Clear the memory cache
evictAll();
return null;
}
}, (Void[])null);
}
/**
* Closes the disk cache associated with this ImageCache object. Note that
* this includes disk access so this should not be executed on the main/UI
* thread.
*/
public void close() {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
if (mDiskCache != null) {
try {
if (!mDiskCache.isClosed()) {
mDiskCache.close();
mDiskCache = null;
}
} catch (final IOException e) {
Log.e(TAG, "close - " + e);
}
}
return null;
}
}, (Void[])null);
}
/**
* Evicts all of the items from the memory cache and lets the system know
* now would be a good time to garbage collect
*/
public void evictAll() {
if (mLruCache != null) {
mLruCache.evictAll();
}
System.gc();
}
/**
* @param key The key used to identify which cache entries to delete.
*/
public void removeFromCache(final String key) {
if (key == null) {
return;
}
// Remove the Lru entry
if (mLruCache != null) {
mLruCache.remove(key);
}
try {
// Remove the disk entry
if (mDiskCache != null) {
mDiskCache.remove(hashKeyForDisk(key));
}
} catch (final IOException e) {
Log.e(TAG, "remove - " + e);
}
flush();
}
/**
* Used to temporarily pause the disk cache while the user is scrolling to
* improve scrolling.
*
* @param pause True to temporarily pause the disk cache, false otherwise.
*/
public void setPauseDiskCache(final boolean pause) {
synchronized (mPauseLock) {
if (mPauseDiskAccess != pause) {
mPauseDiskAccess = pause;
if (!pause) {
mPauseLock.notify();
}
}
}
}
private void waitUntilUnpaused() {
synchronized (mPauseLock) {
if (Looper.myLooper() != Looper.getMainLooper()) {
while (mPauseDiskAccess) {
try {
mPauseLock.wait();
} catch (InterruptedException e) {
// ignored, we'll start waiting again
}
}
}
}
}
/**
* @return True if the user is scrolling, false otherwise.
*/
public boolean isDiskCachePaused() {
return mPauseDiskAccess;
}
/**
* Get a usable cache directory (external if available, internal otherwise)
*
* @param context The {@link Context} to use
* @param uniqueName A unique directory name to append to the cache
* directory
* @return The cache directory
*/
public static final File getDiskCacheDir(final Context context, final String uniqueName) {
// getExternalCacheDir(context) returns null if external storage is not ready
final String cachePath = getExternalCacheDir(context) != null
? getExternalCacheDir(context).getPath()
: context.getCacheDir().getPath();
return new File(cachePath, uniqueName);
}
/**
* Check if external storage is built-in or removable
*
* @return True if external storage is removable (like an SD card), false
* otherwise
*/
public static final boolean isExternalStorageRemovable() {
return Environment.isExternalStorageRemovable();
}
/**
* Get the external app cache directory
*
* @param context The {@link Context} to use
* @return The external cache directory
*/
public static final File getExternalCacheDir(final Context context) {
return context.getExternalCacheDir();
}
/**
* Check how much usable space is available at a given path.
*
* @param path The path to check
* @return The space available in bytes
*/
public static final long getUsableSpace(final File path) {
return path.getUsableSpace();
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable
* for using as a disk filename.
*
* @param key The key used to store the file
*/
public static final String hashKeyForDisk(final String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (final NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
/**
* http://stackoverflow.com/questions/332079
*
* @param bytes The bytes to convert.
* @return A {@link String} converted from the bytes of a hashable key used
* to store a filename on the disk, to hex digits.
*/
private static final String bytesToHexString(final byte[] bytes) {
final StringBuilder builder = new StringBuilder();
for (final byte b : bytes) {
final String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
/**
* A simple non-UI Fragment that stores a single Object and is retained over
* configuration changes. In this sample it will be used to retain an
* {@link ImageCache} object.
*/
public static final class RetainFragment extends Fragment {
/**
* The object to be stored
*/
private Object mObject;
/**
* Empty constructor as per the {@link Fragment} documentation
*/
public RetainFragment() {
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
}
/**
* Store a single object in this {@link Fragment}
*
* @param object The object to store
*/
public void setObject(final Object object) {
mObject = object;
}
/**
* Get the stored object
*
* @return The stored object
*/
public Object getObject() {
return mObject;
}
}
/**
* Used to cache images via {@link LruCache}.
*/
public static final class MemoryCache extends LruCache<String, Bitmap> {
/**
* Constructor of <code>MemoryCache</code>
*
* @param maxSize The allowed size of the {@link LruCache}
*/
public MemoryCache(final int maxSize) {
super(maxSize);
}
/**
* Get the size in bytes of a bitmap.
*/
public static final int getBitmapSize(final Bitmap bitmap) {
return bitmap.getByteCount();
}
/**
* {@inheritDoc}
*/
@Override
protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
return getBitmapSize(paramBitmap);
}
}
}
+333
View File
@@ -0,0 +1,333 @@
/*
* Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
* License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
* or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package com.hippo.ehviewer.cache;
// NOTE: upstream of this class is android.util.LruCache, changes below
// expose trimToSize() to be called externally.
import android.annotation.SuppressLint;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Static library version of {@link android.util.LruCache}. Used to write apps
* that run on API levels prior to 12. When running on API level 12 or above,
* this implementation is still used; it does not try to switch to the
* framework's implementation. See the framework SDK documentation for a class
* overview.
*/
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
private final int maxSize;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other
* caches, this is the maximum sum of the sizes of the entries in
* this cache.
*/
public LruCache(final int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(final K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
this.hitCount++;
return mapValue;
}
this.missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map may
* be different when create() returns. If a conflicting value was added
* to the map while create() was working, we leave that value in the map
* and release the created value.
*/
final V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
this.createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
/* There was a conflict so undo that last put */
this.map.put(key, mapValue);
} else {
this.size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
public final V put(final K key, final V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
this.putCount++;
this.size += safeSizeOf(key, value);
previous = this.map.put(key, value);
if (previous != null) {
this.size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
/**
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
public void trimToSize(final int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (this.size < 0 || this.map.isEmpty() && size != 0) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (this.size <= maxSize || this.map.isEmpty()) {
break;
}
final Map.Entry<K, V> toEvict = this.map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
this.map.remove(key);
this.size -= safeSizeOf(key, value);
this.evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
/**
* Removes the entry for {@code key} if it exists.
*
* @return the previous value mapped by {@code key}.
*/
public final V remove(final K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = this.map.remove(key);
if (previous != null) {
this.size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
* <p>
* The method is called without synchronization: other threads may access
* the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false if
* the removal was caused by a {@link #put} or {@link #remove}.
* @param newValue the new value for {@code key}, if it exists. If non-null,
* this removal was caused by a {@link #put}. Otherwise it was
* caused by an eviction or a {@link #remove}.
*/
protected void entryRemoved(final boolean evicted, final K key, final V oldValue,
final V newValue) {
}
/**
* Called after a cache miss to compute a value for the corresponding key.
* Returns the computed value or null if no value can be computed. The
* default implementation returns null.
* <p>
* The method is called without synchronization: other threads may access
* the cache while this method is executing.
* <p>
* If a value for {@code key} exists in the cache when this method returns,
* the created value will be released with {@link #entryRemoved} and
* discarded. This can occur when multiple threads request the same key at
* the same time (causing multiple values to be created), or when one thread
* calls {@link #put} while another is creating a value for the same key.
*/
protected V create(final K key) {
return null;
}
private int safeSizeOf(final K key, final V value) {
final int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size is
* the number of entries and max size is the maximum number of entries.
* <p>
* An entry's size must not change while it is in the cache.
*/
protected int sizeOf(final K key, final V value) {
return 1;
}
/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
public synchronized final int size() {
return this.size;
}
/**
* For caches that do not override {@link #sizeOf}, this returns the maximum
* number of entries in the cache. For all other caches, this returns the
* maximum sum of the sizes of the entries in this cache.
*/
public synchronized final int maxSize() {
return this.maxSize;
}
/**
* Returns the number of times {@link #get} returned a value.
*/
public synchronized final int hitCount() {
return this.hitCount;
}
/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return this.missCount;
}
/**
* Returns the number of times {@link #create(Object)} returned a value.
*/
public synchronized final int createCount() {
return this.createCount;
}
/**
* Returns the number of times {@link #put} was called.
*/
public synchronized final int putCount() {
return this.putCount;
}
/**
* Returns the number of values that have been evicted.
*/
public synchronized final int evictionCount() {
return this.evictionCount;
}
/**
* Returns a copy of the current contents of the cache, ordered from least
* recently accessed to most recently accessed.
*/
public synchronized final Map<K, V> snapshot() {
return new LinkedHashMap<K, V>(this.map);
}
@SuppressLint("DefaultLocale")
@Override
public synchronized final String toString() {
final int accesses = this.hitCount + this.missCount;
final int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize,
this.hitCount, this.missCount, hitPercent);
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.cardview;
import android.view.View;
/**
* @author Hippo
*
* A salon to make views look like card
*/
public final class CardViewSalon {
@SuppressWarnings("deprecation")
public static void reform(View view, int bgColor, int boundColor) {
view.setBackgroundDrawable(new RoundRectDrawable(
bgColor, boundColor));
}
@SuppressWarnings("deprecation")
public static void reform(View view, int[][] stateSets, int[] bgColors,
int[] boundColors) {
view.setBackgroundDrawable(new RoundRectDrawable(
stateSets, bgColors, boundColors));
}
@SuppressWarnings("deprecation")
public static void reformWithShadow(View view, int bgColor, int boundColor,
boolean keepPadding) {
view.setBackgroundDrawable(new RoundRectDrawableWithShadow(
bgColor, boundColor, keepPadding));
}
@SuppressWarnings("deprecation")
public static void reformWithShadow(View view, int[][] stateSets, int[] bgColors,
int[] boundColors, boolean keepPadding) {
view.setBackgroundDrawable(new RoundRectDrawableWithShadow(
stateSets, bgColors, boundColors, keepPadding));
}
}
@@ -0,0 +1,164 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.cardview;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.StateSet;
import com.hippo.ehviewer.util.Ui;
// Get most code from android.support.v7.widget.CardViewEclairMr1
public class RoundRectDrawable extends Drawable {
protected final float mCornerRadius;
private final int[][] mStateSets;
private final int[] mBgColors;
private final int[] mBoundColors;
private final Paint mBgPaint;
private final Paint mBoundPaint;
private int mLastIndex = -1;
protected final RectF mTempRectF = new RectF();
public RoundRectDrawable(int bgColor, int boundColor) {
mCornerRadius = Ui.dp2pix(2);
mStateSets = null;
mBgColors = null;
mBoundColors = null;
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mBgPaint.setColor(bgColor);
if (boundColor == 0) {
mBoundPaint = null;
} else {
mBoundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mBoundPaint.setColor(boundColor);
}
}
public RoundRectDrawable(int[][] stateSets, int[] bgColors, int[] boundColors) {
mCornerRadius = Ui.dp2pix(2);
mStateSets = stateSets;
mBgColors = bgColors;
mBoundColors = boundColors;
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
if (mBoundColors == null) {
mBoundPaint = null;
} else {
mBoundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
}
}
@Override
public boolean isStateful() {
return mStateSets != null;
}
@Override
protected boolean onStateChange(int[] stateSet) {
final int N = mStateSets.length;
for (int i = 0; i < N; i++) {
if (StateSet.stateSetMatches(mStateSets[i], stateSet)) {
if (mLastIndex == i)
return false;
mLastIndex = i;
mBgPaint.setColor(mBgColors[i]);
if (mBoundColors != null)
mBoundPaint.setColor(mBoundColors[i]);
invalidateSelf();
return true;
}
}
return false;
}
@Override
public void draw(Canvas canvas) {
drawBody(canvas, new RectF(getBounds()));
}
protected void drawBody(Canvas canvas, RectF bounds) {
if (mBoundPaint == null && Build.VERSION.SDK_INT >= 17) {
canvas.drawRoundRect(bounds, mCornerRadius, mCornerRadius, mBgPaint);
} else {
final float twoRadius = mCornerRadius * 2;
final float innerWidth = bounds.width() - twoRadius;
final float innerHeight = bounds.height() - twoRadius;
Paint drawBoundsPaint = mBoundPaint == null ? mBgPaint : mBoundPaint;
// Draw four corner
mTempRectF.set(bounds.left, bounds.top,
bounds.left + mCornerRadius * 2, bounds.top + mCornerRadius * 2);
canvas.drawArc(mTempRectF, 180, 90, true, drawBoundsPaint);
mTempRectF.offset(innerWidth, 0);
canvas.drawArc(mTempRectF, 270, 90, true, drawBoundsPaint);
mTempRectF.offset(0, innerHeight);
canvas.drawArc(mTempRectF, 0, 90, true, drawBoundsPaint);
mTempRectF.offset(-innerWidth, 0);
canvas.drawArc(mTempRectF, 90, 90, true, drawBoundsPaint);
//draw top and bottom pieces
canvas.drawRect(bounds.left + mCornerRadius, bounds.top,
bounds.right - mCornerRadius, bounds.top + mCornerRadius,
drawBoundsPaint);
canvas.drawRect(bounds.left + mCornerRadius,
bounds.bottom - mCornerRadius, bounds.right - mCornerRadius,
bounds.bottom, drawBoundsPaint);
if (mBoundPaint == null) {
// No bounds color, just draw center and left and right at same time
canvas.drawRect(bounds.left, bounds.top + mCornerRadius,
bounds.right, bounds.bottom - mCornerRadius, mBgPaint);
} else {
// Draw left and right pieces
canvas.drawRect(bounds.left, bounds.top + mCornerRadius,
bounds.left + mCornerRadius, bounds.bottom - mCornerRadius,
drawBoundsPaint);
canvas.drawRect(bounds.right - mCornerRadius, bounds.top + mCornerRadius,
bounds.right, bounds.bottom - mCornerRadius, drawBoundsPaint);
// Draw center
canvas.drawRect(bounds.left + mCornerRadius, bounds.top + mCornerRadius,
bounds.right - mCornerRadius, bounds.bottom - mCornerRadius, mBgPaint);
}
}
}
@Override
public void setAlpha(int alpha) {
// not supported
}
@Override
public void setColorFilter(ColorFilter cf) {
// not supported
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.cardview;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import com.hippo.ehviewer.util.Ui;
// Get most code from android.support.v7.widget.RoundRectDrawableWithShadow
public class RoundRectDrawableWithShadow extends RoundRectDrawable {
private final static float SHADOW_MULTIPLIER = 1.5f;
private int mShadowStartColor;
private int mShadowEndColor;
private float mShadowSize;
private Paint mCornerShadowPaint;
private Paint mEdgeShadowPaint;
private final RectF mPreShadowBounds = new RectF();
private final Path mCornerShadowPath = new Path();
private boolean mDirty = true;
private final boolean mKeepPadding;
public RoundRectDrawableWithShadow(int bgColor, int boundColor,
boolean keepPadding) {
super(bgColor, boundColor);
mKeepPadding = keepPadding;
init();
}
public RoundRectDrawableWithShadow(int[][] stateSets,
int[] bgColors, int[] boundColors, boolean keepPadding) {
super(stateSets, bgColors, boundColors);
mKeepPadding = keepPadding;
init();
}
private void init() {
mShadowStartColor = 0x37000000;
mShadowEndColor = 0x03000000;
mShadowSize = Ui.dp2pix(2) * SHADOW_MULTIPLIER;
mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mCornerShadowPaint.setStyle(Paint.Style.FILL);
mCornerShadowPaint.setDither(true);
mEdgeShadowPaint = new Paint(mCornerShadowPaint);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mDirty = true;
}
@Override
public boolean getPadding(Rect padding) {
if (mKeepPadding) {
return false;
} else {
padding.set(getSuggestionPadding());
return true;
}
}
public Rect getSuggestionPadding() {
Rect padding = new Rect();
final int topShadow = (int) Math.ceil(mShadowSize * (1 / (SHADOW_MULTIPLIER * 2)));
final int sideShadow = (int) Math.ceil(mShadowSize - topShadow);
padding.set(sideShadow, topShadow, sideShadow, (int) Math.ceil(mShadowSize));
return padding;
}
private void buildShadowCorners() {
RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
RectF outerBounds = new RectF(innerBounds);
outerBounds.inset(-mShadowSize, -mShadowSize);
mCornerShadowPath.reset();
mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
mCornerShadowPath.moveTo(-mCornerRadius, 0);
mCornerShadowPath.rLineTo(-mShadowSize, 0);
// outer arc
mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
// inner arc
mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
mCornerShadowPath.close();
float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, startRatio, 1f}
, Shader.TileMode.CLAMP));
// we offset the content shadowSize/2 pixels up to make it more realistic.
// this is why edge shadow shader has some extra space
// When drawing bottom edge shadow, we use that extra space.
mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0,
-mCornerRadius - mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
}
private void buildComponents(Rect bounds) {
mPreShadowBounds.set(bounds.left + mShadowSize, bounds.top + mShadowSize,
bounds.right - mShadowSize, bounds.bottom - mShadowSize);
buildShadowCorners();
}
@Override
public void draw(Canvas canvas) {
if (mDirty) {
buildComponents(getBounds());
mDirty = false;
}
drawShadow(canvas);
final float offset = mShadowSize * (1 - 1 / (SHADOW_MULTIPLIER * 2));
final float horizontalOffset = mShadowSize - offset;
canvas.translate(0, -offset);
mPreShadowBounds.bottom += offset;
mPreShadowBounds.left -= horizontalOffset;
mPreShadowBounds.right += horizontalOffset;
drawBody(canvas, mPreShadowBounds);
mPreShadowBounds.bottom -= offset;
mPreShadowBounds.left += horizontalOffset;
mPreShadowBounds.right -= horizontalOffset;
canvas.translate(0, offset);
}
private void drawShadow(Canvas canvas) {
int saved = canvas.save();
float cornerPathSize = 2 * (mCornerRadius + mShadowSize);
float edgeShadowTop = -mCornerRadius - mShadowSize;
final Rect bounds = getBounds();
// LT
canvas.translate(mPreShadowBounds.left + mCornerRadius,
mPreShadowBounds.top + mCornerRadius);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
canvas.drawRect(0, edgeShadowTop,
bounds.right - cornerPathSize, -mCornerRadius,
mEdgeShadowPaint);
// RB
canvas.rotate(180f);
canvas.translate(-bounds.width() + cornerPathSize, -bounds.height() + cornerPathSize);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
canvas.drawRect(0, edgeShadowTop,
bounds.right - cornerPathSize, -mCornerRadius + mShadowSize,
mEdgeShadowPaint);
// LB
canvas.rotate(90f);
canvas.translate(0, -bounds.width() + cornerPathSize);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
canvas.drawRect(0, edgeShadowTop,
bounds.bottom - cornerPathSize, -mCornerRadius,
mEdgeShadowPaint);
// RT
canvas.rotate(180f);
canvas.translate(-bounds.height() + cornerPathSize, -bounds.width() + cornerPathSize);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
canvas.drawRect(0, edgeShadowTop,
bounds.bottom - cornerPathSize, -mCornerRadius,
mEdgeShadowPaint);
canvas.restoreToCount(saved);
}
}
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.util.ArrayList;
import java.util.List;
public class ApiGalleryDetail extends ApiGalleryInfo
implements LofiDetailImpl {
public int previewPageNum;
public int previewPerPage;
private final List<PreviewList> previewLists = new ArrayList<PreviewList>();
public ApiGalleryDetail() {}
public ApiGalleryDetail(ApiGalleryInfo apiGalleryInfo) {
gid = apiGalleryInfo.gid;
token = apiGalleryInfo.token;
title = apiGalleryInfo.title;
posted = apiGalleryInfo.posted;
category = apiGalleryInfo.category;
thumb = apiGalleryInfo.thumb;
uploader = apiGalleryInfo.uploader;
rating = apiGalleryInfo.rating;
simpleLanguage = apiGalleryInfo.simpleLanguage;
archiver_key = apiGalleryInfo.archiver_key;
title_jpn = apiGalleryInfo.title_jpn;
filecount = apiGalleryInfo.filecount;
filesize = apiGalleryInfo.filesize;
expunged = apiGalleryInfo.expunged;
torrentcount = apiGalleryInfo.torrentcount;
apiTags = apiGalleryInfo.apiTags;
}
@Override
public int getGid() {
return gid;
}
@Override
public String getToken() {
return token;
}
@Override
public String getTitle() {
return title;
}
@Override
public int getPreviewPageNum() {
return previewPageNum;
}
@Override
public void setPreviewPageNum(int pageNum) {
previewPageNum = pageNum;
}
@Override
public int getPreviewPerPage() {
return previewPerPage;
}
@Override
public PreviewList getPreview(int page) {
if (page >= 0 && page < previewLists.size())
return previewLists.get(page);
else
return null;
}
@Override
public void setPreview(int page, PreviewList previewList) {
if (page >= 0 && page < previewLists.size())
previewLists.set(page, previewList);
else if (page == previewLists.size())
previewLists.add(previewList);
else
; // Do nothing
}
@Override
public String[] getTags() {
return apiTags;
}
@Override
public GalleryInfo toGalleryInfo() {
return new GalleryInfo(this);
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import android.os.Parcel;
import android.os.Parcelable;
import com.hippo.ehviewer.util.Utils;
public class ApiGalleryInfo extends GalleryInfo{
public String archiver_key;
public String title_jpn;
public String filecount;
public long filesize;
public boolean expunged;
public String torrentcount;
public String[] apiTags;
public static final Parcelable.Creator<ApiGalleryInfo> CREATOR =
new Parcelable.Creator<ApiGalleryInfo>() {
@Override
public ApiGalleryInfo createFromParcel(Parcel source) {
ApiGalleryInfo p = new ApiGalleryInfo();
p.gid = source.readInt();
p.token = source.readString();
p.title = source.readString();
p.posted = source.readString();
p.category = source.readInt();
p.thumb = source.readString();
p.uploader = source.readString();
p.rating = source.readFloat();
p.simpleLanguage = source.readString();
p.archiver_key = source.readString();
p.title_jpn = source.readString();
p.filecount = source.readString();
p.filesize = source.readLong();
p.expunged = Utils.int2boolean(source.readInt());
p.torrentcount = source.readString();
int apiTagsLength = source.readInt();
p.apiTags = new String[apiTagsLength];
for (int i = 0; i < apiTagsLength; i++)
p.apiTags[i] = source.readString();
return p;
}
@Override
public ApiGalleryInfo[] newArray(int size) {
return new ApiGalleryInfo[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(archiver_key);
dest.writeString(title_jpn);
dest.writeString(filecount);
dest.writeLong(filesize);
dest.writeInt(Utils.boolean2int(expunged));
dest.writeString(torrentcount);
dest.writeInt(apiTags.length);
for (String str : apiTags)
dest.writeString(str);
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
public class Comment {
public String time;
public String user;
public String comment;
public Comment (String time, String user, String comment) {
this.time = time;
this.user = user;
this.comment = comment;
}
}
@@ -0,0 +1,934 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.SparseArray;
import com.hippo.ehviewer.Analytics;
import com.hippo.ehviewer.service.DownloadService;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Log;
import com.hippo.ehviewer.util.MathUtils;
import com.hippo.ehviewer.util.SqlUtils;
// TODO
// what a mess
// refactoring
/**
* @author Hippo
*
* Ver 2: 移除 read 表,添加 Download 表
* Ver 3: Download 表添加下载时间项
* Ver 4: 添历史记录
*/
public class Data {
private static final String TAG = "Data";
private static final int VERSION = 4;
private static final String DB_NAME = "data";
private static final String TABLE_GALLERY = "gallery";
private static final String TABLE_LOCAL_FAVOURITE = "local_favourite";
private static final String TABLE_TAG = "tag";
private static final String TABLE_DOWNLOAD = "download";
private static final String TABLE_HISTORY = "history";
private static final String COLUMN_GID = "gid";
private static final String COLUMN_TOKEN = "token";
private static final String COLUMN_TITLE = "title";
private static final String COLUMN_POSTED = "posted";
private static final String COLUMN_CATEGORY = "category";
private static final String COLUMN_THUMB = "thumb";
private static final String COLUMN_UPLOADER = "uploader";
private static final String COLUMN_RATING = "rating";
private static final String COLUMN_REFERENCE = "reference";
private static final String COLUMN_ID = "id";
private static final String COLUMN_NAME = "name";
private static final String COLUMN_MODE = "mode";
private static final String COLUMN_SEARCH = "search";
private static final String COLUMN_ADVANCE = "advance";
private static final String COLUMN_MIN_RATING = "min_rating";
private static final String COLUMN_TAG = "tag";
private static final String COLUMN_STATE = "state";
private static final String COLUMN_LEGACY = "legacy";
private static final String COLUMN_DATE = "date";
private final SparseArray<GalleryInfo> mGallerys;
private long mTagRowNum;
private List<Tag> mTags;
private List<GalleryInfo> mLocalFavourites;
private List<DownloadInfo> mDownloads;
private final Context mContext;
private final DBHelper mDBHelper;
private final SQLiteDatabase mDatabase;
private static Data sInstance;
public static void createInstance(Context context) {
sInstance = new Data(context);
}
public static Data getInstance() {
return sInstance;
}
public Data(Context context) {
mContext = context;
mGallerys = new SparseArray<GalleryInfo>();
mDBHelper = new DBHelper(mContext);
mDatabase = mDBHelper.getWritableDatabase();
getTags();
getLocalFavourites();
getDownloads();
}
@Override
public void finalize() {
mDBHelper.close();
}
/****** gallery ******/
/**
* Get galleryInfo in table, if do not exits return null
*
* @param gid
* @return
*/
public GalleryInfo getGallery(SQLiteDatabase db, int gid) {
GalleryInfo galleryInfo = mGallerys.get(gid);
if (galleryInfo == null) {
Cursor cursor = db.rawQuery("select * from " + TABLE_GALLERY + " where " + COLUMN_GID + "=?",
new String[]{String.valueOf(gid)});
if (cursor.moveToFirst()) {
galleryInfo = new GalleryInfo();
galleryInfo.gid = gid;
galleryInfo.token = cursor.getString(1);
galleryInfo.title = cursor.getString(2);
galleryInfo.posted = cursor.getString(3);
galleryInfo.category = cursor.getInt(4);
galleryInfo.thumb = cursor.getString(5);
galleryInfo.uploader = cursor.getString(6);
galleryInfo.rating = cursor.getFloat(7);
galleryInfo.generateSLang();
// add to map
mGallerys.put(gid, galleryInfo);
}
cursor.close();
}
return galleryInfo;
}
@SuppressWarnings("unused")
private synchronized boolean containsGallery(int gid) {
boolean re = false;
Cursor cursor = mDatabase.rawQuery("select * from "
+ TABLE_GALLERY + " where " + COLUMN_GID + "=?",
new String[]{String.valueOf(gid)});
if (cursor.moveToFirst())
re = true;
cursor.close();
return re;
}
/**
* If do not contain the gallery return -1;
* @param gid
* @return
*/
private synchronized int getGalleryReference(int gid) {
int reference = -1;
Cursor cursor = mDatabase.rawQuery("select " + COLUMN_REFERENCE + " from "
+ TABLE_GALLERY + " where " + COLUMN_GID + "=?",
new String[]{String.valueOf(gid)});
if (cursor.moveToFirst()) {
reference = cursor.getInt(0);
}
cursor.close();
return reference;
}
/**
*
* If exits same gid, add the reference by 1.
* If exits no same gid, add it to table reference is 1
*
* @param galleryInfo
*/
private void addGallery(GalleryInfo galleryInfo) {
boolean isInsert;
int gid = galleryInfo.gid;
// add to map
mGallerys.put(gid, galleryInfo);
// add to sql or update and add reference by 1
int reference;
if ((reference = getGalleryReference(gid)) != -1) {
isInsert = false;
reference++;
}
else {
isInsert = true;
reference = 1;
}
ContentValues values = new ContentValues();
values.put(COLUMN_GID, gid);
values.put(COLUMN_TOKEN, galleryInfo.token);
values.put(COLUMN_TITLE, galleryInfo.title);
values.put(COLUMN_POSTED, galleryInfo.posted);
values.put(COLUMN_CATEGORY, galleryInfo.category);
values.put(COLUMN_THUMB, galleryInfo.thumb);
values.put(COLUMN_UPLOADER, galleryInfo.uploader);
values.put(COLUMN_RATING, galleryInfo.rating);
values.put(COLUMN_REFERENCE, reference);
if (isInsert) { // Set
mDatabase.insert(TABLE_GALLERY, null, values);
} else { // Update
mDatabase.update(TABLE_GALLERY, values, COLUMN_GID + "=?", new String[]{String.valueOf(gid)});
}
}
/**
* True if update
*
* @param gid
* @return
*/
private boolean updateGallery(GalleryInfo galleryInfo) {
ContentValues values = new ContentValues();
values.put(COLUMN_GID, galleryInfo.gid);
values.put(COLUMN_TOKEN, galleryInfo.token);
values.put(COLUMN_TITLE, galleryInfo.title);
values.put(COLUMN_POSTED, galleryInfo.posted);
values.put(COLUMN_CATEGORY, galleryInfo.category);
values.put(COLUMN_THUMB, galleryInfo.thumb);
values.put(COLUMN_UPLOADER, galleryInfo.uploader);
values.put(COLUMN_RATING, galleryInfo.rating);
return mDatabase.update(TABLE_GALLERY, values,
COLUMN_GID + "=?", new String[]{String.valueOf(galleryInfo.gid)}) != 0;
}
/**
* decrease reference by 1, only delete when reference is 0
* @param gid
*/
private boolean deleteGallery(int gid) {
int reference = getGalleryReference(gid);
if (reference == -1) {
Log.w(TAG, "Can't get reference when deleteGallery, gid is " + gid);
return false;
}
reference--;
if (reference == 0) { // delete gallery
// delete in sql
mDatabase.delete(TABLE_GALLERY, COLUMN_GID + "=?", new String[]{String.valueOf(gid)});
// delete in list
mGallerys.remove(gid);
} else { // update reference
ContentValues values = new ContentValues();
values.put(COLUMN_REFERENCE, reference);
mDatabase.update(TABLE_GALLERY, values, COLUMN_GID + "=?", new String[]{String.valueOf(gid)});
}
return true;
}
/****** download ******/
private synchronized void getDownloads() {
boolean startService = false;
boolean keepDownloadService = Config.getKeepDownloadService();
mDownloads = new ArrayList<DownloadInfo>();
Cursor cursor = mDatabase.rawQuery("select * from " + TABLE_DOWNLOAD
+ " order by " + COLUMN_DATE + " asc", null);
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
int gid = cursor.getInt(0);
GalleryInfo galleryInfo = getGallery(mDatabase, gid);
if (galleryInfo == null) {
cursor.moveToNext();
continue;
}
int mode = cursor.getInt(1);
int state = cursor.getInt(2);
int legacy = cursor.getInt(3);
if (state == DownloadInfo.STATE_WAIT || state == DownloadInfo.STATE_DOWNLOAD) {
if (keepDownloadService) {
state = DownloadInfo.STATE_WAIT;
startService = true;
} else {
state = DownloadInfo.STATE_NONE;
}
}
DownloadInfo downloadInfo = new DownloadInfo(
galleryInfo, mode, state, legacy);
mDownloads.add(downloadInfo);
cursor.moveToNext();
}
}
cursor.close();
if (startService) {
Intent it = new Intent(mContext, DownloadService.class);
mContext.startService(it);
}
}
/**
* Return null if not found
* @return
*/
public synchronized DownloadInfo getFirstWaitDownloadInfo() {
for (DownloadInfo di : mDownloads) {
if (di.state == DownloadInfo.STATE_WAIT)
return di;
}
return null;
}
/**
* True if contain this item
*
* @param gid
* @return
*/
public synchronized boolean containsDownload(int gid) {
boolean re = false;
Cursor cursor = mDatabase.rawQuery("select * from "
+ TABLE_DOWNLOAD + " where " + COLUMN_GID + "=?",
new String[]{String.valueOf(gid)});
if (cursor.moveToFirst())
re = true;
cursor.close();
return re;
}
/**
* If exits, just update
*
* @param downloadInfo
* @return
*/
public synchronized boolean addDownload(DownloadInfo downloadInfo) {
GalleryInfo galleryInfo = downloadInfo.galleryInfo;
int gid = galleryInfo.gid;
// Add to download
boolean update;
if (containsDownload(gid)) {
update = true;
} else {
update = false;
addGallery(galleryInfo);
}
ContentValues values = new ContentValues();
values.put(COLUMN_GID, gid);
values.put(COLUMN_MODE, downloadInfo.mode);
values.put(COLUMN_STATE, downloadInfo.state);
values.put(COLUMN_LEGACY, downloadInfo.legacy);
if (update) {
mDatabase.update(TABLE_DOWNLOAD, values,
COLUMN_GID + "=?", new String[]{String.valueOf(galleryInfo.gid)});
return false;
} else {
// If not update, set date
values.put(COLUMN_DATE, System.currentTimeMillis());
if (mDatabase.insert(TABLE_DOWNLOAD, null,
values) != -1) { // If add ok
mDownloads.add(downloadInfo);
return true;
} else { // If add fail
deleteGallery(gid);
return false;
}
}
}
public synchronized boolean deleteDownload(int gid) {
int deleteNum = mDatabase.delete(TABLE_DOWNLOAD, COLUMN_GID + "=?", new String[]{String.valueOf(gid)});
if (deleteNum > 1)
Log.w(TAG, "WTF? more than one gid is " + gid);
if (deleteNum == 0)
return false;
// delete from list
for (DownloadInfo di : mDownloads) {
if (gid == di.galleryInfo.gid) {
mDownloads.remove(di);
break;
}
}
// sub reference
deleteGallery(gid);
return true;
}
public synchronized List<DownloadInfo> getAllDownloads() {
return mDownloads;
}
/**
* Return null if not found
*
* @param gid
* @return
*/
public synchronized DownloadInfo getDownload(int gid) {
for (DownloadInfo di : mDownloads) {
if (gid == di.galleryInfo.gid)
return di;
}
return null;
}
public synchronized void setDownloadState(int gid, int state) {
setDownloadState(getDownload(gid), state);
}
public synchronized void setDownloadState(DownloadInfo di, int state) {
if (di == null || di.state == state)
return;
di.state = state;
// Update in db
ContentValues values = new ContentValues();
values.put(COLUMN_STATE, state);
mDatabase.update(TABLE_DOWNLOAD, values,
COLUMN_GID + "=?", new String[]{String.valueOf(di.galleryInfo.gid)});
}
/**
* If state is STATE_NONE or state is STATE_FINISH and legacy is not 0,
* make state STATE_WAIT
*/
public synchronized void startAllDownload() {
for (DownloadInfo di : mDownloads) {
if (di.state == DownloadInfo.STATE_NONE ||
(di.state == DownloadInfo.STATE_FINISH && di.legacy != 0)) {
setDownloadState(di, DownloadInfo.STATE_WAIT);
}
}
}
/**
* If state is STATE_WAIT or STATE_DOWNLOAD,
* make state STATE_NONE
*/
public synchronized void stopAllDownload() {
for (DownloadInfo di : mDownloads) {
if (di.state == DownloadInfo.STATE_WAIT ||
di.state == DownloadInfo.STATE_DOWNLOAD) {
setDownloadState(di, DownloadInfo.STATE_NONE);
}
}
}
/****** local favourite ******/
private synchronized void getLocalFavourites() {
mLocalFavourites = new ArrayList<GalleryInfo>();
Cursor cursor = mDatabase.rawQuery("select * from "
+ TABLE_LOCAL_FAVOURITE, null);
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
int gid = cursor.getInt(0);
GalleryInfo galleryInfo = getGallery(mDatabase, gid);
if (galleryInfo != null)
mLocalFavourites.add(galleryInfo);
cursor.moveToNext();
}
}
cursor.close();
}
public synchronized boolean containsLocalFavourite(int gid) {
boolean re = false;
Cursor cursor = mDatabase.rawQuery("select * from "
+ TABLE_LOCAL_FAVOURITE + " where " + COLUMN_GID + "=?",
new String[]{String.valueOf(gid)});
if (cursor.moveToFirst())
re = true;
cursor.close();
return re;
}
/**
* galleryInfo can't be null
* @param galleryInfo
*/
public synchronized void addLocalFavourite(GalleryInfo galleryInfo) {
int gid = galleryInfo.gid;
if (containsLocalFavourite(gid)) {
updateGallery(galleryInfo);
} else {
addGallery(galleryInfo);
// add to sql
ContentValues values = new ContentValues();
values.put(COLUMN_GID, galleryInfo.gid);
mDatabase.insert(TABLE_LOCAL_FAVOURITE, null, values);
// add to list
mLocalFavourites.add(galleryInfo);
// Analytics
Analytics.addToFavoriteGallery(mContext, gid, galleryInfo.token);
}
}
public synchronized void deleteLocalFavourite(int gid) {
// delete from list
for (GalleryInfo galleryInfo : mLocalFavourites) {
if (galleryInfo.gid == gid) {
mLocalFavourites.remove(galleryInfo);
break;
}
}
// delete from sql
mDatabase.delete(TABLE_LOCAL_FAVOURITE, COLUMN_GID + "=?", new String[]{String.valueOf(gid)});
deleteGallery(gid);
}
public synchronized List<GalleryInfo> getAllLocalFavourites() {
return mLocalFavourites;
}
/****** tag ******/
private synchronized void getTags() {
mTags = new ArrayList<Tag>();
mTagRowNum = 0;
Cursor cursor = mDatabase.rawQuery("select * from " + TABLE_TAG
+ " order by " + COLUMN_ID + " asc", null);
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
String name = cursor.getString(1);
int mode = cursor.getInt(2);
int category = cursor.getInt(3);
String search = cursor.getString(4);
int advance = cursor.getInt(5);
int min_rating = cursor.getInt(6);
String tagStr = cursor.getString(7);
Tag tag = new Tag(name, category, search);
tag.setAdvance(advance, min_rating);
tag.setTag(tagStr);
tag.setMode(mode);
mTags.add(tag);
cursor.moveToNext();
mTagRowNum++;
}
}
cursor.close();
}
public synchronized boolean addTag(Tag tag) {
ContentValues values = new ContentValues();
values.put(COLUMN_ID, mTagRowNum);
values.put(COLUMN_NAME, tag.getName());
values.put(COLUMN_MODE, tag.getMode());
values.put(COLUMN_CATEGORY, tag.getCategory());
values.put(COLUMN_SEARCH, tag.getSearch());
values.put(COLUMN_ADVANCE, tag.getAdvanceType());
values.put(COLUMN_MIN_RATING, tag.getMinRating());
values.put(COLUMN_TAG, tag.getTag());
boolean ok = mDatabase.insert(
TABLE_TAG, null, values) != -1;
if (ok) {
mTagRowNum++;
mTags.add(tag);
}
return ok;
}
public synchronized boolean deleteTag(int id) {
int deleteNum = mDatabase.delete(TABLE_TAG, COLUMN_ID + "=?", new String[]{String.valueOf(id)});
if (deleteNum > 1)
Log.e(TAG, "WTF? more than one id is " + id);
if (deleteNum == 0)
return false;
if (id < 0 || id >= mTags.size()) {
Log.e(TAG, "id is out of bounds, id is " + id + ", mTags.size() is " + mTags.size());
return false;
}
// Update sql
ContentValues values = new ContentValues();
for (int i = id + 1; i < mTagRowNum; i++) {
values.put(COLUMN_ID, i - 1);
mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(i)});
}
mTagRowNum -= deleteNum;
// Update list
mTags.remove(id);
return true;
}
public synchronized List<Tag> getAllTags() {
return mTags;
}
public synchronized Tag getTag(int location) {
return mTags.get(location);
}
public synchronized List<String> getAllTagNames() {
List<String> names = new ArrayList<String>();
for (Tag tag : mTags)
names.add(tag.getName());
return names;
}
/**
* location >= 0 && location <= tagRowNum please
* @param location
* @param tag
*/
public synchronized void setTag(int location, Tag tag) {
if (location == mTagRowNum) {
addTag(tag);
} else {
// Update list
mTags.set(location, tag);
// Update sql
ContentValues values = new ContentValues();
values.put(COLUMN_NAME, tag.getName());
values.put(COLUMN_CATEGORY, tag.getCategory());
values.put(COLUMN_SEARCH, tag.getSearch());
values.put(COLUMN_ADVANCE, tag.getAdvanceType());
values.put(COLUMN_MIN_RATING, tag.getMinRating());
mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(location)});
}
}
public synchronized boolean swapTag(int indexOne, int indexTwo) {
if (indexOne < 0 || indexOne > mTagRowNum
|| indexTwo < 0 || indexTwo > mTagRowNum)
return false;
if (indexOne == indexTwo)
return true;
// Update list
Tag temp = mTags.get(indexOne);
mTags.set(indexOne, mTags.get(indexTwo));
mTags.set(indexTwo, temp);
// Update sql
ContentValues values = new ContentValues();
values.put(COLUMN_ID, -1);
mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(indexOne)});
values.put(COLUMN_ID, indexOne);
mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(indexTwo)});
values.put(COLUMN_ID, indexTwo);
mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(-1)});
return true;
}
public static final int NONE = 0x0;
public static final int BROWSE = 0x1;
public static final int READ = 0x2;
public synchronized List<HistoryItem> getHistory(int mode, boolean desc) {
List<HistoryItem> history = new ArrayList<HistoryItem>();
StringBuilder sb = new StringBuilder();
sb.append("select * from ").append(TABLE_HISTORY);
boolean first = true;
if (mode != 0) {
sb.append(" where " + COLUMN_MODE + " in (");
for (int b = 1; b <= READ; b <<= 1) {
if ((mode & b) != 0) {
sb.append(first ? "" : ", ").append(b);
first = false;
}
}
sb.append(")");
}
sb.append(" order by ").append(COLUMN_DATE).append(desc ? " desc" : " asc");
Log.d(TAG, sb.toString());
Cursor cursor = mDatabase.rawQuery(sb.toString(), null);
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
int gid = cursor.getInt(0);
GalleryInfo gi = getGallery(mDatabase, gid);
if (gi != null) {
history.add(new HistoryItem(gi, cursor.getInt(1), cursor.getLong(2)));
}
cursor.moveToNext();
}
}
cursor.close();
return history;
}
public synchronized int containsHistory(int gid) {
int re;
Cursor cursor = mDatabase.rawQuery("select * from "
+ TABLE_HISTORY + " where " + COLUMN_GID + "=?",
new String[]{String.valueOf(gid)});
if (cursor.moveToFirst())
re = cursor.getInt(1);
else
re = NONE;
cursor.close();
return re;
}
private int checkMode(int mode) {
return MathUtils.clamp(mode, NONE, READ);
}
public synchronized void addHistory(GalleryInfo gi, int mode) {
mode = checkMode(mode);
long curDate = System.currentTimeMillis();
ContentValues values = new ContentValues();
values.put(COLUMN_GID, gi.gid);
values.put(COLUMN_DATE, curDate);
int curMode = checkMode(containsHistory(gi.gid));
if (curMode == NONE) {
// Add new record
values.put(COLUMN_MODE, mode);
mDatabase.insert(TABLE_HISTORY, null, values);
addGallery(gi);
truncateHistory(Config.getMaxHistoryCount());
} else {
// Update record
int newMode = Math.max(curMode, mode);
values.put(COLUMN_MODE, newMode);
mDatabase.update(TABLE_HISTORY, values,
COLUMN_GID + "=?", new String[]{String.valueOf(gi.gid)});
updateGallery(gi);
}
}
public void clearHistory() {
truncateHistory(0);
}
protected synchronized void truncateHistory(int maxEntries) {
if (maxEntries < 0) {
return;
} else if (maxEntries == 0) {
mDatabase.delete(TABLE_HISTORY, null, null);
} else {
try {
mDatabase.execSQL("delete from " + TABLE_HISTORY + " where " + COLUMN_GID
+ " in " + "(select " + COLUMN_GID + " from " + TABLE_HISTORY
+ " order by " + COLUMN_DATE + " desc limit " + maxEntries
+ ", -1);");
} catch(Throwable e){}
}
}
public class DBHelper extends SQLiteOpenHelper {
public DBHelper(Context context) {
super(context, DB_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// gallery
String CreateGallery = "create table " + TABLE_GALLERY + "("
+ COLUMN_GID + " integer primary key,"
+ COLUMN_TOKEN + " text,"
+ COLUMN_TITLE + " text,"
+ COLUMN_POSTED + " text,"
+ COLUMN_CATEGORY + " integer,"
+ COLUMN_THUMB + " text,"
+ COLUMN_UPLOADER + " text,"
+ COLUMN_RATING + " float,"
+ COLUMN_REFERENCE + " integer);";
db.execSQL(CreateGallery);
// local favourite
String CreateLocalFavourite = "create table "
+ TABLE_LOCAL_FAVOURITE + "("
+ COLUMN_GID + " integer primary key,"
+ "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));";
db.execSQL(CreateLocalFavourite);
// tag
String CreateTag = "create table "
+ TABLE_TAG + "("
+ COLUMN_ID + " integer primary key,"
+ COLUMN_NAME + " text,"
+ COLUMN_MODE + " integer,"
+ COLUMN_CATEGORY + " integer,"
+ COLUMN_SEARCH + " text,"
+ COLUMN_ADVANCE + " integer,"
+ COLUMN_MIN_RATING + " integer,"
+ COLUMN_TAG + " text);";
db.execSQL(CreateTag);
// download
createDownloadTableVer3(db);
// History
createHistoryTable(db);
}
private void createDownloadTableVer2(SQLiteDatabase db) {
String createDownload = "create table "
+ TABLE_DOWNLOAD + "("
+ COLUMN_GID + " integer primary key,"
+ COLUMN_MODE + " integer,"
+ COLUMN_STATE + " integer,"
+ COLUMN_LEGACY + " integer,"
+ "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));";
SqlUtils.exeSQLSafely(db, createDownload);
}
private synchronized List<DownloadInfo> getDownloadsVer2(SQLiteDatabase db) {
List<DownloadInfo> list = new ArrayList<DownloadInfo>();
Cursor cursor = db.rawQuery("select * from " + TABLE_DOWNLOAD,
null);
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
int gid = cursor.getInt(0);
GalleryInfo galleryInfo = getGallery(db, gid);
if (galleryInfo == null) {
cursor.moveToNext();
continue;
}
int mode = cursor.getInt(1);
int state = cursor.getInt(2);
int legacy = cursor.getInt(3);
if (state == DownloadInfo.STATE_WAIT || state == DownloadInfo.STATE_DOWNLOAD)
state = DownloadInfo.STATE_NONE;
DownloadInfo downloadInfo = new DownloadInfo(
galleryInfo, mode, state, legacy);
list.add(downloadInfo);
cursor.moveToNext();
}
}
cursor.close();
return list;
}
private void createDownloadTableVer3(SQLiteDatabase db) {
String createDownload = "create table "
+ TABLE_DOWNLOAD + "("
+ COLUMN_GID + " integer primary key,"
+ COLUMN_MODE + " integer,"
+ COLUMN_STATE + " integer,"
+ COLUMN_LEGACY + " integer,"
+ COLUMN_DATE + " long,"
+ "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));";
SqlUtils.exeSQLSafely(db, createDownload);
}
public void addDownloadInfoVer3(SQLiteDatabase db, List<DownloadInfo> l) {
for (int i = 0; i < l.size(); i++) {
DownloadInfo di = l.get(i);
ContentValues values = new ContentValues();
values.put(COLUMN_GID, di.galleryInfo.gid);
values.put(COLUMN_MODE, di.mode);
values.put(COLUMN_STATE, di.state);
values.put(COLUMN_LEGACY, di.legacy);
values.put(COLUMN_DATE, (long) i);
db.insert(TABLE_DOWNLOAD, null, values);
}
}
private void createHistoryTable(SQLiteDatabase db) {
String createDownload = "create table "
+ TABLE_HISTORY + "("
+ COLUMN_GID + " integer primary key,"
+ COLUMN_MODE + " integer,"
+ COLUMN_DATE + " long,"
+ "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));";
SqlUtils.exeSQLSafely(db, createDownload);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
SqlUtils.dropTable(db, TABLE_DOWNLOAD);
SqlUtils.dropTable(db, "read");
createDownloadTableVer2(db);
case 2:
List<DownloadInfo> l = getDownloadsVer2(db);
SqlUtils.dropTable(db, TABLE_DOWNLOAD);
createDownloadTableVer3(db);
addDownloadInfoVer3(db, l);
case 3:
createHistoryTable(db);
case VERSION:
break;
default:
SqlUtils.dropAllTable(db);
onCreate(db);
break;
}
}
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import com.hippo.ehviewer.widget.ExpandableListItem;
public class DownloadInfo implements ExpandableListItem {
@SuppressWarnings("unused")
private static final String TAG = DownloadInfo.class.getSimpleName();
public static final int STATE_NONE = 0x0;
public static final int STATE_WAIT = 0x1;
public static final int STATE_DOWNLOAD = 0x2;
public static final int STATE_FINISH = 0x3;
public final GalleryInfo galleryInfo;
public final int mode;
public volatile int state = STATE_NONE;
/** byte/second **/
public volatile int speed;
public volatile int download = -1;
public volatile int total = -1;
public volatile int legacy;
private boolean mExpanded = false;
public DownloadInfo(GalleryInfo gi, int mode) {
this.galleryInfo = gi;
this.mode = mode;
}
public DownloadInfo(GalleryInfo gi, int mode, int state, int legacy) {
this.galleryInfo = gi;
this.mode = mode;
this.state = state;
this.legacy = legacy;
}
@Override
public void setExpanded(boolean expanded) {
mExpanded = expanded;
}
@Override
public boolean getExpanded() {
return mExpanded;
}
}
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
public class GalleryDetail extends GalleryInfo
implements PreviewImpl {
public String title_jpn;
public int pages;
public String size;
public String resized;
public String parent;
public String visible;
public String language;
public int people;
public String firstPage;
/**
* Can't be null, just can be empty
*/
public LinkedHashMap<String, LinkedList<String>> tags;
// For Preview
public int previewSum;
public int previewPerPage;
public PreviewList[] previewLists;
public List<Comment> comments;
public GalleryDetail() {}
public GalleryDetail(GalleryInfo galleryInfo) {
gid = galleryInfo.gid;
token = galleryInfo.token;
title = galleryInfo.title;
posted = galleryInfo.posted;
category = galleryInfo.category;
thumb = galleryInfo.thumb;
uploader = galleryInfo.uploader;
rating = galleryInfo.rating;
simpleLanguage = galleryInfo.simpleLanguage;
}
@Override
public int getGid() {
return gid;
}
@Override
public String getToken() {
return token;
}
@Override
public String getTitle() {
return title;
}
@Override
public int getPreviewPageNum() {
return previewSum;
}
@Override
public int getPreviewPerPage() {
return previewPerPage;
}
@Override
public PreviewList getPreview(int page) {
if (page >= 0 && previewLists != null && page < previewLists.length)
return previewLists[page];
else
return null;
}
@Override
public void setPreview(int page, PreviewList previewList) {
if (previewLists == null && previewSum > 0)
previewLists = new PreviewList[previewSum];
if (page >= 0 && previewLists != null && page < previewLists.length)
previewLists[page] = previewList;
}
@Override
public GalleryInfo toGalleryInfo() {
return new GalleryInfo(this);
}
}
@@ -0,0 +1,174 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.util.regex.Pattern;
import android.os.Parcel;
import android.os.Parcelable;
public class GalleryInfo implements Parcelable {
/**
* ISO 639-1
*/
public static final String S_LANG_JA = "JA";
public static final String S_LANG_EN = "EN";
public static final String S_LANG_ZH = "ZH";
public static final String S_LANG_NL = "NL";
public static final String S_LANG_FR = "FR";
public static final String S_LANG_DE = "DE";
public static final String S_LANG_HU = "HU";
public static final String S_LANG_IT = "IT";
public static final String S_LANG_KO = "KO";
public static final String S_LANG_PL = "PL";
public static final String S_LANG_PT = "PT";
public static final String S_LANG_RU = "RU";
public static final String S_LANG_ES = "ES";
public static final String S_LANG_TH = "TH";
public static final String S_LANG_VI = "VI";
public static final String[] S_LANGS = {
S_LANG_EN,
S_LANG_ZH,
S_LANG_ES,
S_LANG_KO,
S_LANG_RU,
S_LANG_FR,
S_LANG_PT,
S_LANG_TH,
S_LANG_DE,
S_LANG_IT,
S_LANG_VI,
S_LANG_PL,
S_LANG_HU,
S_LANG_NL,
};
public static final String[] S_LANG_PATTERNS = {
"[(\\[]eng(?:lish)?[)\\]]",
"[((\\[]chinese[))\\]]|[汉漢]化|中[国國][语語]|中文|[(\\[]CN[)\\]]",
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]",
"[(\\[]korean?[)\\]]",
"[(\\[]rus(?:sian)?[)\\]]",
"[(\\[]fr(?:ench)?[)\\]]",
"[(\\[]portuguese",
"[(\\[]thai(?: ภาษาไทย)?[)\\]]",
"[(\\[]german[)\\]]",
"[(\\[]italiano?[)\\]]",
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]",
"[(\\[]polish[)\\]]",
"[(\\[]hun(?:garian)?[)\\]]",
"[(\\[]dutch[)\\]]",
};
public int gid;
public String token;
public String title;
public String posted;
public int category;
public String thumb;
public String uploader;
public float rating;
/**
* Will not be recond
*/
public int thumbWidth;
/**
* Will not be recond
*/
public int thumbHeight;
/**
* language get from title
*/
public String simpleLanguage;
public static final Parcelable.Creator<GalleryInfo> CREATOR =
new Parcelable.Creator<GalleryInfo>() {
@Override
public GalleryInfo createFromParcel(Parcel source) {
GalleryInfo p = new GalleryInfo();
p.gid = source.readInt();
p.token = source.readString();
p.title = source.readString();
p.posted = source.readString();
p.category = source.readInt();
p.thumb = source.readString();
p.uploader = source.readString();
p.rating = source.readFloat();
p.simpleLanguage = source.readString();
return p;
}
@Override
public GalleryInfo[] newArray(int size) {
return new GalleryInfo[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(gid);
dest.writeString(token);
dest.writeString(title);
dest.writeString(posted);
dest.writeInt(category);
dest.writeString(thumb);
dest.writeString(uploader);
dest.writeFloat(rating);
dest.writeString(simpleLanguage);
}
public GalleryInfo() {
// Empty
}
public GalleryInfo(GalleryInfo gi) {
gid = gi.gid;
token = gi.token;
title = gi.title;
posted = gi.posted;
category = gi.category;
thumb = gi.thumb;
uploader = gi.uploader;
rating = gi.rating;
simpleLanguage = gi.simpleLanguage;
}
@Override
public boolean equals(Object o) {
if (o instanceof GalleryInfo && ((GalleryInfo) o).gid == gid)
return true;
else
return false;
}
public final void generateSLang() {
for (int i = 0; i < S_LANGS.length; i++) {
if (Pattern.compile(S_LANG_PATTERNS[i], Pattern.CASE_INSENSITIVE).matcher(title).find()) {
simpleLanguage = S_LANGS[i];
return;
}
}
simpleLanguage = null;
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import android.os.Parcel;
import android.os.Parcelable;
public class GalleryPopular extends GalleryInfo {
public long count;
public static final Parcelable.Creator<GalleryPopular> CREATOR =
new Parcelable.Creator<GalleryPopular>() {
@Override
public GalleryPopular createFromParcel(Parcel source) {
GalleryPopular p = new GalleryPopular();
p.gid = source.readInt();
p.token = source.readString();
p.title = source.readString();
p.posted = source.readString();
p.category = source.readInt();
p.thumb = source.readString();
p.uploader = source.readString();
p.rating = source.readFloat();
p.simpleLanguage = source.readString();
p.count = source.readLong();
return p;
}
@Override
public GalleryPopular[] newArray(int size) {
return new GalleryPopular[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(count);
}
}
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import android.os.Parcel;
import android.os.Parcelable;
public class HistoryItem extends GalleryInfo {
public int mode;
public long date;
public static final Parcelable.Creator<HistoryItem> CREATOR =
new Parcelable.Creator<HistoryItem>() {
@Override
public HistoryItem createFromParcel(Parcel source) {
HistoryItem p = new HistoryItem();
p.gid = source.readInt();
p.token = source.readString();
p.title = source.readString();
p.posted = source.readString();
p.category = source.readInt();
p.thumb = source.readString();
p.uploader = source.readString();
p.rating = source.readFloat();
p.simpleLanguage = source.readString();
p.mode = source.readInt();
p.date = source.readLong();
return p;
}
@Override
public HistoryItem[] newArray(int size) {
return new HistoryItem[size];
}
};
public HistoryItem() {
// Empty
}
public HistoryItem(GalleryInfo gi, int m, long d) {
super(gi);
mode = m;
date = d;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mode);
dest.writeLong(date);
}
}
@@ -0,0 +1,126 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.util.ArrayList;
import java.util.List;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.hippo.ehviewer.ImageLoader;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.widget.SimpleGridLayout;
public class LargePreviewList extends PreviewList {
private class Item {
public String mImageUrl;
public String mPageUrl;
public Item(String imageUrl, String pageUrl) {
mImageUrl = imageUrl;
mPageUrl = pageUrl;
}
}
public List<Item> mItemList = new ArrayList<Item>();
public void addItem(String imageUrl, String pageUrl) {
mItemList.add(new Item(imageUrl, pageUrl));
}
@Override
public String getPageUrl(int index) {
if (index >= 0 && index < mItemList.size())
return mItemList.get(index).mPageUrl;
return null;
}
@Override
public int size() {
return mItemList.size();
}
@SuppressLint("InflateParams")
@Override
public void addPreview(SimpleGridLayout viewGroup) {
if (mTargetPage != mHolder.getCurPreviewPage())
return;
viewGroup.removeAllViews();
LayoutInflater inflater = LayoutInflater.from(mActivity);
int startIndex = mTargetPage * mPi.getPreviewPerPage();
int index = startIndex;
for (Item item : mItemList) {
View view = inflater.inflate(R.layout.preview_item, null);
((TextView)view.findViewById(R.id.text)).setText(String.valueOf(index + 1));
final int _index = index;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Add to read in Data
Intent intent = new Intent(mActivity,
GalleryActivity.class);
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mPi.toGalleryInfo());
intent.putExtra(GalleryActivity.KEY_START_INDEX, _index);
mActivity.startActivity(intent);
}
});
viewGroup.addView(view);
// TODO I need a better key
ImageLoader.getInstance(mActivity).add(item.mImageUrl, mPi.getGid() +
"-preview-" + index,
new PreviewImageGetListener(viewGroup, index - startIndex));
index++;
}
}
private class PreviewImageGetListener implements ImageLoader.OnGetImageListener {
private final SimpleGridLayout mViewGroup;
private final int mIndex;
/**
* index is view index in viewgroup
* @param viewGroup
* @param index
*/
public PreviewImageGetListener(SimpleGridLayout viewGroup, int index) {
mViewGroup = viewGroup;
mIndex = index;
}
@Override
public void onGetImage(String key, Bitmap bmp, int state) {
if (mTargetPage != mHolder.getCurPreviewPage())
return;
if (bmp == null)
mHolder.onGetPreviewImageFailure();
else
((ImageView)mViewGroup.getChildAt(mIndex).findViewById(R.id.image)).setImageBitmap(bmp);
}
}
}
@@ -0,0 +1,334 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import com.hippo.ehviewer.ehclient.EhClient;
import com.hippo.ehviewer.network.UrlBuilder;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Utils;
public class ListUrls {
public static final boolean NUM_CATEGORY = false;
// Mode
public static final int MODE_NORMAL = 0x0;
public static final int MODE_UPLOADER = 0x1;
public static final int MODE_TAG = 0x2;
public static final int MODE_POPULAR = 0x3;
public static final int MODE_IMAGE_SEARCH = 0x4;
// Category
public static final int NONE = -1; // Use it for homepage
public static final int MISC = 0x1;
public static final int DOUJINSHI = 0x2;
public static final int MANGA = 0x4;
public static final int ARTIST_CG = 0x8;
public static final int GAME_CG = 0x10;
public static final int IMAGE_SET = 0x20;
public static final int COSPLAY = 0x40;
public static final int ASIAN_PORN = 0x80;
public static final int NON_H = 0x100;
public static final int WESTERN = 0x200;
public static final int UNKNOWN = 0x400;
public static final int ALL_CATEGORT = UNKNOWN - 1;
//DOUJINSHI|MANGA|ARTIST_CG|GAME_CG|WESTERN|NON_H|IMAGE_SET|COSPLAY|ASIAN_PORN|MISC;
// advance search
public static final int SNAME = 0x1;
public static final int STAGS = 0x2;
public static final int SDESC = 0x4;
public static final int STORR = 0x8;
public static final int STO = 0x10;
public static final int STD1 = 0x20;
public static final int STD2 = 0x40;
public static final int SH = 0x80;
public static final int DEFAULT_ADVANCE = SNAME | STAGS;
public static final int DEFAULT_MIN_RATING = 2;
private int mMode = MODE_NORMAL;
private int page = 0;
private int category = NONE;
private int advsearchType = -1;
private int minRating = -1;
private String search = null;
private String mTag = null;
private int mImageSearchMode;
private String mImageKey;
private String mImageUrl;
private File mSearchFile;
private String mResultUrl = null;
public ListUrls() {}
public ListUrls(int category) {
this.category = category;
}
public ListUrls(int category, int page) {
this.category = category;
this.page = page;
}
public ListUrls(int category, String search) {
this.category = category;
this.search = search;
}
public ListUrls(int category, String search, int page) {
this.category = category;
this.search = search;
this.page = page;
}
public void setCategory(int category) {
this.category = category;
}
public int getCategory() {
return category;
}
public String getSearch() {
return search;
}
public void setAdvance(boolean enabled) {
if (enabled && advsearchType == -1)
advsearchType = DEFAULT_ADVANCE;
else if (!enabled)
advsearchType = -1;
}
public void setAdvance(int advanceType) {
advsearchType = advanceType;
}
public void setAdvance(int advanceType, int minRating) {
advsearchType = advanceType;
this.minRating = minRating;
}
public boolean isAdvance() {
return advsearchType != -1;
}
public int getAdvanceType() {
return advsearchType;
}
public boolean isMinRating() {
return minRating != -1;
}
public int getMinRating() {
return minRating;
}
public void setPage(int p) {
page = p;
}
public int getPage() {
return page;
}
public int getMode() {
return mMode;
}
public void setMode(int mode) {
mMode = mode;
}
// For tag mode
public void setTag(String tag) {
mMode = MODE_TAG;
mTag = tag;
}
public String getTag() {
return mTag;
}
// For image search mode
public void setSearchImage(String key, String url, int imageSearchMode) {
mMode = MODE_IMAGE_SEARCH;
mImageKey = key;
mImageUrl = url;
mImageSearchMode = imageSearchMode;
}
public void setSearchFile(File searchFile, int imageSearchMode) {
mMode = MODE_IMAGE_SEARCH;
mSearchFile = searchFile;
mImageSearchMode = imageSearchMode;
}
public void setSearchResult(String resultUrl) {
mResultUrl = resultUrl;
}
public File getSearchFile() {
return mSearchFile;
}
public String getSearchImageKey() {
return mImageKey;
}
public String getSearchImageUrl() {
return mImageUrl;
}
public int getImageSearchMode() {
return mImageSearchMode;
}
public String getUrl() {
return getUrl(Config.getMode());
}
public String getUrl(int mode) {
switch (mMode) {
case MODE_POPULAR:
return null;
case MODE_IMAGE_SEARCH:
if (mResultUrl == null)
return null;
else
return mResultUrl + "&page=" + page;
case MODE_TAG:
// Add tag
StringBuilder sb = new StringBuilder(EhClient.getUrlHeader(mode));
sb.append("tag/");
if (mTag != null) {
String[] tag = mTag.split("\\s+");
try {
for (int i = 0; i < tag.length; i++)
tag[i] = URLEncoder.encode(tag[i], "UTF-8");
} catch (UnsupportedEncodingException e) {}
sb.append(Utils.join(tag, '+'));
// Add page
sb.append("/").append(page);
} else {
sb.append(page);
}
return sb.toString();
case MODE_NORMAL:
case MODE_UPLOADER:
default:
String header = EhClient.getUrlHeader(mode);
boolean isNeedFooter = false;
UrlBuilder ub;
// Add category
if (category != NONE) {
isNeedFooter = true;
if (NUM_CATEGORY) {
ub = new UrlBuilder(header + category);
} else {
ub = new UrlBuilder(header);
ub.addQuery("f_doujinshi", ((category & DOUJINSHI) == 0) ? "0" : "1");
ub.addQuery("f_manga", ((category & MANGA) == 0) ? "0" : "1");
ub.addQuery("f_artistcg", ((category & ARTIST_CG) == 0) ? "0" : "1");
ub.addQuery("f_gamecg", ((category & GAME_CG) == 0) ? "0" : "1");
ub.addQuery("f_western", ((category & WESTERN) == 0) ? "0" : "1");
ub.addQuery("f_non-h", ((category & NON_H) == 0) ? "0" : "1");
ub.addQuery("f_imageset", ((category & IMAGE_SET) == 0) ? "0" : "1");
ub.addQuery("f_cosplay", ((category & COSPLAY) == 0) ? "0" : "1");
ub.addQuery("f_asianporn", ((category & ASIAN_PORN) == 0) ? "0" : "1");
ub.addQuery("f_misc", ((category & MISC) == 0) ? "0" : "1");
}
} else {
ub = new UrlBuilder(header);
}
// Add search item
if (search != null) {
isNeedFooter = true;
String[] tag = search.split("\\s+");
try {
for (int i = 0; i < tag.length; i++)
tag[i] = URLEncoder.encode(tag[i], "UTF-8");
} catch (UnsupportedEncodingException e) {}
ub.addQuery("f_search", Utils.join(tag, '+'));
}
// Add page
if (page != 0) {
ub.addQuery("page", page);
}
// Add foot
if (isNeedFooter) ub.addQuery("f_apply", "Apply+Filter");
// Add advance search
if (isAdvance()) {
ub.addQuery("advsearch", "1");
if((advsearchType & SNAME) != 0) ub.addQuery("f_sname", "on");
if((advsearchType & STAGS) != 0) ub.addQuery("f_stags", "on");
if((advsearchType & SDESC) != 0) ub.addQuery("f_sdesc", "on");
if((advsearchType & STORR) != 0) ub.addQuery("f_storr", "on");
if((advsearchType & STO) != 0) ub.addQuery("f_sto", "on");
if((advsearchType & STD1) != 0) ub.addQuery("f_sdt1", "on");
if((advsearchType & STD2) != 0) ub.addQuery("f_sdt2", "on");
if((advsearchType & SH) != 0) ub.addQuery("f_sh", "on");
// Set min star
if (isMinRating()) {
ub.addQuery("f_sr", "on");
ub.addQuery("f_srdd", minRating);
}
}
return ub.build();
}
}
@Override
public String toString() {
switch (mMode) {
case MODE_NORMAL:
case MODE_UPLOADER:
return search;
case MODE_TAG:
return mTag;
case MODE_POPULAR:
case MODE_IMAGE_SEARCH:
default:
return null;
}
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
public interface LofiDetailImpl extends PreviewImpl {
public void setPreviewPageNum(int pageNum);
/**
* Check null and empty before use
*
* @return
*/
public String[] getTags();
}
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.util.ArrayList;
import java.util.List;
public class LofiGalleryDetail extends LofiGalleryInfo
implements LofiDetailImpl {
public int previewPageNum;
public int previewPerPage;
private final List<PreviewList> previewLists = new ArrayList<PreviewList>();
public LofiGalleryDetail(GalleryInfo galleryInfo) {
this(new LofiGalleryInfo(galleryInfo));
}
public LofiGalleryDetail(LofiGalleryInfo lofiGalleryInfo) {
gid = lofiGalleryInfo.gid;
token = lofiGalleryInfo.token;
title = lofiGalleryInfo.title;
posted = lofiGalleryInfo.posted;
category = lofiGalleryInfo.category;
thumb = lofiGalleryInfo.thumb;
uploader = lofiGalleryInfo.uploader;
rating = lofiGalleryInfo.rating;
simpleLanguage = lofiGalleryInfo.simpleLanguage;
lofiTags = lofiGalleryInfo.lofiTags;
previewPageNum = Integer.MAX_VALUE;
}
@Override
public int getGid() {
return gid;
}
@Override
public String getToken() {
return token;
}
@Override
public String getTitle() {
return title;
}
@Override
public int getPreviewPageNum() {
return previewPageNum;
}
@Override
public void setPreviewPageNum(int pageNum) {
previewPageNum = pageNum;
}
@Override
public int getPreviewPerPage() {
return previewPerPage;
}
@Override
public PreviewList getPreview(int page) {
if (page >= 0 && page < previewLists.size())
return previewLists.get(page);
else
return null;
}
@Override
public void setPreview(int page, PreviewList previewList) {
if (page >= 0 && page < previewLists.size())
previewLists.set(page, previewList);
else if (page == previewLists.size())
previewLists.add(previewList);
else
; // Do nothing
}
@Override
public String[] getTags() {
return lofiTags;
}
@Override
public GalleryInfo toGalleryInfo() {
return new GalleryInfo(this);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import android.os.Parcel;
import android.os.Parcelable;
public class LofiGalleryInfo extends GalleryInfo {
/**
* It could be null, please check before use.
*/
public String[] lofiTags;
public static final Parcelable.Creator<LofiGalleryInfo> CREATOR =
new Parcelable.Creator<LofiGalleryInfo>() {
@Override
public LofiGalleryInfo createFromParcel(Parcel source) {
int length;
LofiGalleryInfo p = new LofiGalleryInfo();
p.gid = source.readInt();
p.token = source.readString();
p.title = source.readString();
p.posted = source.readString();
p.category = source.readInt();
p.thumb = source.readString();
p.uploader = source.readString();
p.rating = source.readFloat();
p.simpleLanguage = source.readString();
length = source.readInt();
if (length > 0) {
p.lofiTags = new String[length];
for (int i = 0; i < length; i++)
p.lofiTags[i] = source.readString();
}
return p;
}
@Override
public LofiGalleryInfo[] newArray(int size) {
return new LofiGalleryInfo[size];
}
};
public LofiGalleryInfo() {}
public LofiGalleryInfo(GalleryInfo galleryInfo) {
gid = galleryInfo.gid;
token = galleryInfo.token;
title = galleryInfo.title;
posted = galleryInfo.posted;
category = galleryInfo.category;
thumb = galleryInfo.thumb;
uploader = galleryInfo.uploader;
rating = galleryInfo.rating;
simpleLanguage = galleryInfo.simpleLanguage;
lofiTags = null;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
int length = lofiTags == null ? 0 : lofiTags.length;
dest.writeInt(length);
for (int i = 0; i < length; i++)
dest.writeString(lofiTags[i]);
}
}
@@ -0,0 +1,182 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import java.util.ArrayList;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.hippo.ehviewer.ImageLoader;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.widget.SimpleGridLayout;
public class NormalPreviewList extends PreviewList{
public class Item {
public int xOffset;
public int yOffset;
public int width;
public int height;
public String url;
public Item(int xOffset, int yOffset, int width, int height, String url) {
this.xOffset = xOffset;
this.yOffset = yOffset;
this.width = width;
this.height = height;
this.url = url;
}
}
public class Row {
public String imageUrl;
public ArrayList<Item> itemArray = new ArrayList<Item>();
public int startIndex;
public Row(String imageUrl) {
this.imageUrl = imageUrl;
}
public void addItem(int xOffset, int yOffset, int width, int height,
String url) {
itemArray.add(new Item(xOffset, yOffset, width, height, url));
}
}
private Row curRow;
public ArrayList<Row> rowArray = new ArrayList<Row>();
public void addItem(String imageUrl, String xOffset, String yOffset, String width, String height,
String url) {
if (curRow == null) {
curRow = new Row(imageUrl);
curRow.startIndex = 0;
rowArray.add(curRow);
} else if (!curRow.imageUrl.equals(imageUrl)) {
Row lastRow = curRow;
curRow = new Row(imageUrl);
curRow.startIndex = lastRow.startIndex + lastRow.itemArray.size();
rowArray.add(curRow);
}
curRow.addItem(Integer.parseInt(xOffset),
Integer.parseInt(yOffset), Integer.parseInt(width),
Integer.parseInt(height), url);
}
@Override
public int size() {
int sum = 0;
for (Row row : rowArray)
sum += row.itemArray.size();
return sum;
}
@Override
public String getPageUrl(int index) {
for (Row row : rowArray) {
if (index < row.startIndex + row.itemArray.size() && index >= row.startIndex)
return row.itemArray.get(index - row.startIndex).url;
}
return null;
}
// TODO reuse the TextView in AutoWrapLayout
@SuppressLint("InflateParams")
@Override
public void addPreview(SimpleGridLayout viewGroup) {
if (mTargetPage != mHolder.getCurPreviewPage())
return;
viewGroup.removeAllViews();
LayoutInflater inflater = LayoutInflater.from(mActivity);
int index = mTargetPage * mPi.getPreviewPerPage(); // it is display index
int rowIndex = 0;
for (NormalPreviewList.Row row : rowArray) {
for (int i = 0; i < row.itemArray.size(); i++) {
View view = inflater.inflate(R.layout.preview_item, null);
((TextView)view.findViewById(R.id.text)).setText(String.valueOf(index + 1));
final int _index = index;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Add to read in Data
Intent intent = new Intent(mActivity,
GalleryActivity.class);
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mPi.toGalleryInfo());
intent.putExtra(GalleryActivity.KEY_START_INDEX, _index);
mActivity.startActivity(intent);
}
});
viewGroup.addView(view);
index++;
}
// TODO I need a better key
ImageLoader.getInstance(mActivity).add(row.imageUrl, mPi.getGid() +
"-preview-" + mTargetPage + "-" + rowIndex,
new PreviewImageGetListener(viewGroup, rowIndex));
rowIndex++;
}
}
private class PreviewImageGetListener implements ImageLoader.OnGetImageListener {
private final SimpleGridLayout mViewGroup;
private final int mRowIndex;
public PreviewImageGetListener(SimpleGridLayout viewGroup, int rowIndex) {
mViewGroup = viewGroup;
mRowIndex = rowIndex;
}
@Override
public void onGetImage(String key, Bitmap bmp, int state) {
if (mTargetPage != mHolder.getCurPreviewPage())
return;
if (bmp == null) {
mHolder.onGetPreviewImageFailure();
} else {
int maxWidth = bmp.getWidth();
int maxHeight = bmp.getHeight();
NormalPreviewList.Row row = rowArray.get(mRowIndex);
int i = row.startIndex;
for(Item item : row.itemArray) {
if (item.xOffset + item.width > maxWidth)
item.width = maxWidth - item.xOffset;
if (item.yOffset + item.height > maxHeight)
item.height = maxHeight - item.yOffset;
Bitmap bitmap = Bitmap.createBitmap(bmp, item.xOffset, item.yOffset, item.width, item.height);
((ImageView)mViewGroup.getChildAt(i).findViewById(R.id.image)).setImageBitmap(bitmap);
i++;
}
}
}
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
public interface PreviewImpl {
public int getGid();
public String getToken();
public String getTitle();
/**
* If it is unkonwn, return Integer.MAX_VALUE
*
* @return
*/
public int getPreviewPageNum();
public int getPreviewPerPage();
/**
* If page is error, return null
*
* @param page
* @return
*/
public PreviewList getPreview(int page);
/**
*
* @param previewList
*/
public void setPreview(int page, PreviewList previewList);
public GalleryInfo toGalleryInfo();
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
import android.app.Activity;
import android.content.Context;
import android.widget.TextView;
import com.hippo.ehviewer.widget.SimpleGridLayout;
/**
* This interface describe the previews in one page
*
* @author Hippo
*
*/
public abstract class PreviewList {
protected PreviewHolder mHolder;
protected Activity mActivity;
protected PreviewImpl mPi;
protected int mTargetPage;
/**
* Add preview to view group
* @param viewGroup
* @return
*/
public abstract void addPreview(SimpleGridLayout viewGroup);
public abstract int size();
public void setData(PreviewHolder holder, Activity activity, PreviewImpl pi, int targetPage) {
mHolder = holder;
mActivity = activity;
mPi = pi;
mTargetPage = targetPage;
}
public void setTargetPage(int targetPage) {
mTargetPage = targetPage;
}
/**
* Return page url at target index, null for invalid index
* @param index
* @return
*/
public abstract String getPageUrl(int index);
protected class TextViewWithUrl extends TextView {
public String url;
public TextViewWithUrl(Context context) {
super(context);
}
}
public interface PreviewHolder {
public int getCurPreviewPage();
public void onGetPreviewImageFailure();
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.data;
public class Tag extends ListUrls {
private String mName;
public Tag(String name, int category, String search) {
super(category, search);
mName = name;
}
public Tag(String name, ListUrls lus) {
this(name, lus.getCategory(), lus.getSearch());
setAdvance(lus.getAdvanceType(), lus.getMinRating());
setTag(lus.getTag());
setMode(lus.getMode());
}
public void setName(String name) {
mName = name;
}
public String getName() {
return mName;
}
}
@@ -0,0 +1,172 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.drawable;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import com.hippo.ehviewer.util.MathUtils;
public class BatteryDrawable extends Drawable {
@SuppressWarnings("unused")
private static final String TAG = BatteryDrawable.class.getSimpleName();
public static final int WARN_LIMIT = 15;
private int mColor = Color.WHITE;
private int mWarnColor = Color.RED;
private int mElect = -1;
private final Paint mPaint;
private final Rect mTopRect;
private final Rect mBottomRect;
private final Rect mRightRect;
private final Rect mHeadRect;
private final Rect mElectRect;
private int mStart;
private int mStop;
public BatteryDrawable() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mTopRect = new Rect();
mBottomRect = new Rect();
mRightRect = new Rect();
mHeadRect = new Rect();
mElectRect = new Rect();
updatePaint();
}
@Override
protected void onBoundsChange(Rect bounds) {
int width = bounds.width();
int height = bounds.height();
int strokeWidth = (int) (Math.sqrt(width * width + height * height) * 0.06f);
int turn1 = width * 6 / 7;
int turn2 = height / 3;
int secBottom = height - strokeWidth;
mStart = strokeWidth;
mStop = turn1 - strokeWidth;
mTopRect.set(0, 0, turn1, strokeWidth);
mBottomRect.set(0, secBottom, turn1, height);
mRightRect.set(turn1 - strokeWidth, strokeWidth, turn1, secBottom);
mHeadRect.set(turn1, turn2, width, height - turn2);
mElectRect.set(0, strokeWidth, mStop, secBottom);
}
/**
* How to draw:<br>
* |------------------------------|<br>
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
* |------------------------------|---|<br>
* |/////////////////| |//|\\\|<br>
* |/////////////////| |//|\\\|<br>
* |------------------------------|---|<br>
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
* |------------------------------|
*/
@Override
public void draw(Canvas canvas) {
if (mElect == -1)
return;
mElectRect.right = MathUtils.lerp(mStart, mStop, mElect / 100.0f);
canvas.drawRect(mTopRect, mPaint);
canvas.drawRect(mBottomRect, mPaint);
canvas.drawRect(mRightRect, mPaint);
canvas.drawRect(mHeadRect, mPaint);
canvas.drawRect(mElectRect, mPaint);
}
private boolean isWarn() {
return mElect <= WARN_LIMIT;
}
public void setColor(int color) {
if (mColor == color)
return;
mColor = color;
if (!isWarn()) {
mPaint.setColor(mColor);
invalidateSelf();
}
}
public void setWarnColor(int color) {
if (mWarnColor == color)
return;
mWarnColor = color;
if (isWarn()) {
mPaint.setColor(mWarnColor);
invalidateSelf();
}
}
public void setElect(int elect) {
if (mElect == elect)
return;
mElect = elect;
updatePaint();
}
public void setElect(int elect, boolean warn) {
if (mElect == elect)
return;
mElect = elect;
updatePaint(warn);
}
private void updatePaint() {
updatePaint(isWarn());
}
private void updatePaint(boolean warn) {
if (warn)
mPaint.setColor(mWarnColor);
else
mPaint.setColor(mColor);
invalidateSelf();
}
@Override
public void setAlpha(int alpha) {
// Empty
}
@Override
public void setColorFilter(ColorFilter cf) {
// Empty
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
@@ -0,0 +1,187 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.drawable;
import java.util.Arrays;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import com.hippo.ehviewer.util.MathUtils;
import com.hippo.ehviewer.util.Ui;
public class FreeMaterialDrawable extends Drawable {
private static final String TAG = FreeMaterialDrawable.class.getSimpleName();
private static final int MAX_SPRITE_NUM = 6;
private static final int SPRITE_SHAPE_RECTANGLE = 0;
private static final int SPRITE_SHAPE_ROUND = 1;
private static final int SPRITE_SHAPE_MAX = 2;
private boolean mKeepTop;
private int mBgIndex;
private int mBgColor;
private final Paint mPaint;
private int mSpriteNum;
private final Sprite[] mSpriteArray = new Sprite[MAX_SPRITE_NUM];
private static final int[] sColorArray = new int[] {
0xff9c27b0, 0xff673ab7,
0xff3f51b5, 0xff2196f3, 0xff03a9f4, 0xff00bcd4,
0xff4caf50
};
public FreeMaterialDrawable () {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setShadowLayer(Ui.dp2pix(3), 0, 0, 0x8a000000);
}
public FreeMaterialDrawable setBgIndex(int bgIndex) {
mBgIndex = bgIndex;
return this;
}
public FreeMaterialDrawable setKeepTop(boolean keepTop) {
mKeepTop = keepTop;
return this;
}
private int getColor(int[] copyOfColorArray) {
int index = MathUtils.random(0, copyOfColorArray.length);
int color = copyOfColorArray[index];
while (color == -1)
color = copyOfColorArray[++index < copyOfColorArray.length ? index : (index = 0)];
copyOfColorArray[index] = -1;
return color;
}
@Override
protected void onBoundsChange(Rect bounds) {
int w = bounds.width();
int h = bounds.height();
Sprite s;
int[] copyOfColorArray = Arrays.copyOf(sColorArray, sColorArray.length);
if (mBgIndex >= 0) {
mBgColor = copyOfColorArray[mBgIndex];
copyOfColorArray[mBgIndex] = -1;
} else {
mBgColor = getColor(copyOfColorArray);
}
mSpriteNum = MathUtils.random(4, MAX_SPRITE_NUM + 1);
for (int i = 0; i < mSpriteNum; i++) {
switch (SPRITE_SHAPE_ROUND) {//MathUtils.random(0, SPRITE_SHAPE_MAX)) {
case SPRITE_SHAPE_RECTANGLE:
s = new Rectangle(w, h, getColor(copyOfColorArray));
break;
default:
case SPRITE_SHAPE_ROUND:
s = new Round(w, h, getColor(copyOfColorArray));
break;
}
mSpriteArray[i] = s;
}
}
@Override
public void draw(Canvas canvas) {
canvas.drawColor(mBgColor);
for (int i = 0; i < mSpriteNum; i++)
mSpriteArray[i].draw(canvas, mPaint);
}
@Override
public void setAlpha(int alpha) {
// Empty
}
@Override
public void setColorFilter(ColorFilter cf) {
// Empty
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
private abstract class Sprite {
private final int mColor;
public Sprite(int w, int h, int color) {
mColor = color;
}
public void draw(Canvas canvas, Paint paint) {
paint.setColor(mColor);
doDraw(canvas, paint);
}
public abstract void doDraw(Canvas canvas, Paint paint);
}
private class Rectangle extends Sprite {
private final int left;
private final int top;
private final int right;
private final int bottom;
public Rectangle(int w, int h, int color) {
super(w, h, color);
int width = MathUtils.random(w / 4, w / 2);
int height = MathUtils.random(h / 4, h / 2);
left = MathUtils.random(w - width / 2);
top = MathUtils.random(h - height / 2);
right = left + width;
bottom = top + height;
}
@Override
public void doDraw(Canvas canvas, Paint paint) {
canvas.drawRect(left, top, right, bottom, paint);
}
}
private class Round extends Sprite {
int mX, mY, mRadius;
public Round(int w, int h, int color) {
super(w, h, color);
int t = (w + h) / 2;
mRadius = MathUtils.random(t / 8, t / 4);
mX = MathUtils.random(w);
mY = MathUtils.random(mKeepTop ? mRadius + 10 : 0, h);
}
@Override
public void doDraw(Canvas canvas, Paint paint) {
canvas.drawCircle(mX, mY, mRadius, paint);
}
}
}
@@ -0,0 +1,695 @@
/*
* Copyright (C) 2014 Balys Valentukevicius
*
* 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.hippo.ehviewer.drawable;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.PixelFormat;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Property;
import android.util.TypedValue;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
public class MaterialIndicatorDrawable extends Drawable implements Animatable {
private static final String STATE_KEY = "material_menu_icon_state";
public enum IconState {
BURGER, ARROW
}
public enum AnimationState {
BURGER_ARROW;
public IconState getFirstState() {
switch (this) {
case BURGER_ARROW:
return IconState.BURGER;
default:
return null;
}
}
public IconState getSecondState() {
switch (this) {
case BURGER_ARROW:
return IconState.ARROW;
default:
return null;
}
}
}
public enum Stroke {
/**
* 3 dip
*/
REGULAR(3),
/**
* 2 dip
*/
THIN(2),
/**
* 1 dip
*/
EXTRA_THIN(1);
private final int strokeWidth;
Stroke(int strokeWidth) {
this.strokeWidth = strokeWidth;
}
protected static Stroke valueOf(int strokeWidth) {
switch (strokeWidth) {
case 3:
return REGULAR;
case 2:
return THIN;
case 1:
return EXTRA_THIN;
default:
return THIN;
}
}
}
public static final int DEFAULT_COLOR = Color.WHITE;
public static final int DEFAULT_SCALE = 1;
public static final int DEFAULT_TRANSFORM_DURATION = 800;
public static final int DEFAULT_PRESSED_DURATION = 400;
private static final int BASE_DRAWABLE_WIDTH = 40;
private static final int BASE_DRAWABLE_HEIGHT = 40;
private static final int BASE_ICON_WIDTH = 20;
private static final int BASE_CIRCLE_RADIUS = 18;
private static final float ARROW_MID_LINE_ANGLE = 180;
private static final float ARROW_TOP_LINE_ANGLE = 135;
private static final float ARROW_BOT_LINE_ANGLE = 225;
private static final float TRANSFORMATION_START = 0;
private static final float TRANSFORMATION_MID = 1.0f;
private static final float TRANSFORMATION_END = 2.0f;
private static final int DEFAULT_CIRCLE_ALPHA = 200;
private final float diph;
private final float dip1;
private final float dip2;
private final float dip3;
private final float dip4;
private final int width;
private final int height;
private final float strokeWidth;
private final float iconWidth;
private final float topPadding;
private final float sidePadding;
private final float circleRadius;
private final Stroke stroke;
private final Object lock = new Object();
private final Paint gridPaint = new Paint();
private final Paint iconPaint = new Paint();
private final Paint circlePaint = new Paint();
private float transformationValue = 0f;
private float pressedProgressValue = 0f;
private boolean transformationRunning = false;
private IconState currentIconState = IconState.BURGER;
private AnimationState animationState = AnimationState.BURGER_ARROW;
private IconState animatingIconState;
private boolean drawTouchCircle;
private boolean neverDrawTouch;
private boolean rtlEnabled;
private ObjectAnimator transformation;
private ObjectAnimator pressedCircle;
private MaterialMenuState materialMenuState;
public MaterialIndicatorDrawable(Context context, int color, Stroke stroke) {
this(context, color, stroke, DEFAULT_SCALE, DEFAULT_TRANSFORM_DURATION, DEFAULT_PRESSED_DURATION);
}
public MaterialIndicatorDrawable(Context context, int color, Stroke stroke, int transformDuration, int pressedDuration) {
this(context, color, stroke, DEFAULT_SCALE, transformDuration, pressedDuration);
}
public MaterialIndicatorDrawable(Context context, int color, Stroke stroke, int scale, int transformDuration, int pressedDuration) {
Resources resources = context.getResources();
// convert each separately due to various densities
this.dip1 = dpToPx(resources, 1) * scale;
this.dip2 = dpToPx(resources, 2) * scale;
this.dip3 = dpToPx(resources, 3) * scale;
this.dip4 = dpToPx(resources, 4) * scale;
this.diph = dip1 / 2;
this.stroke = stroke;
this.width = (int) (dpToPx(resources, BASE_DRAWABLE_WIDTH) * scale);
this.height = (int) (dpToPx(resources, BASE_DRAWABLE_HEIGHT) * scale);
this.iconWidth = dpToPx(resources, BASE_ICON_WIDTH) * scale;
this.circleRadius = dpToPx(resources, BASE_CIRCLE_RADIUS) * scale;
this.strokeWidth = dpToPx(resources, stroke.strokeWidth) * scale;
this.sidePadding = (width - iconWidth) / 2;
this.topPadding = (height - 5 * dip3) / 2;
initPaint(color);
initAnimations(transformDuration, pressedDuration);
materialMenuState = new MaterialMenuState();
}
private MaterialIndicatorDrawable(int color, Stroke stroke, long transformDuration, long pressedDuration,
int width, int height, float iconWidth, float circleRadius, float strokeWidth, float dip1
) {
this.dip1 = dip1;
this.dip2 = dip1 * 2;
this.dip3 = dip1 * 3;
this.dip4 = dip1 * 4;
this.diph = dip1 / 2;
this.stroke = stroke;
this.width = width;
this.height = height;
this.iconWidth = iconWidth;
this.circleRadius = circleRadius;
this.strokeWidth = strokeWidth;
this.sidePadding = (width - iconWidth) / 2;
this.topPadding = (height - 5 * dip3) / 2;
initPaint(color);
initAnimations((int) transformDuration, (int) pressedDuration);
materialMenuState = new MaterialMenuState();
}
private void initPaint(int color) {
gridPaint.setAntiAlias(false);
gridPaint.setColor(Color.GREEN);
gridPaint.setStrokeWidth(1);
iconPaint.setAntiAlias(true);
iconPaint.setStyle(Style.STROKE);
iconPaint.setStrokeWidth(strokeWidth);
iconPaint.setColor(color);
circlePaint.setAntiAlias(true);
circlePaint.setStyle(Style.FILL);
circlePaint.setColor(color);
circlePaint.setAlpha(DEFAULT_CIRCLE_ALPHA);
setBounds(0, 0, width, height);
}
/*
* Drawing
*/
@Override public void draw(Canvas canvas) {
final float ratio = transformationValue <= 1 ? transformationValue : 2 - transformationValue;
if (rtlEnabled) {
canvas.save();
canvas.scale(-1, 1, 0, 0);
canvas.translate(-getIntrinsicWidth(), 0);
}
drawTopLine(canvas, ratio);
drawMiddleLine(canvas, ratio);
drawBottomLine(canvas, ratio);
if (rtlEnabled) {
canvas.restore();
}
if (drawTouchCircle) drawTouchCircle(canvas);
}
private void drawTouchCircle(Canvas canvas) {
canvas.restore();
canvas.drawCircle(width / 2, height / 2, pressedProgressValue, circlePaint);
}
private void drawMiddleLine(Canvas canvas, float ratio) {
canvas.restore();
canvas.save();
float rotation = 0;
float pivotX = width / 2;
float pivotY = width / 2;
float startX = sidePadding;
float startY = topPadding + dip3 / 2 * 5;
float stopX = width - sidePadding;
float stopY = topPadding + dip3 / 2 * 5;
int alpha = 255;
switch (animationState) {
case BURGER_ARROW:
// rotate by 180
if (isMorphingForward()) {
rotation = ratio * ARROW_MID_LINE_ANGLE;
} else {
rotation = ARROW_MID_LINE_ANGLE + (1 - ratio) * ARROW_MID_LINE_ANGLE;
}
// shorten one end
stopX -= ratio * resolveStrokeModifier(ratio) / 2;
break;
}
iconPaint.setAlpha(alpha);
canvas.rotate(rotation, pivotX, pivotY);
canvas.drawLine(startX, startY, stopX, stopY, iconPaint);
iconPaint.setAlpha(255);
}
private void drawTopLine(Canvas canvas, float ratio) {
canvas.save();
float rotation = 0, pivotX = 0, pivotY = 0;
float rotation2 = 0;
// pivot at center of line
float pivotX2 = width / 2 + dip3 / 2;
float pivotY2 = topPadding + dip2;
float startX = sidePadding;
float startY = topPadding + dip2;
float stopX = width - sidePadding;
float stopY = topPadding + dip2;
int alpha = 255;
switch (animationState) {
case BURGER_ARROW:
if (isMorphingForward()) {
// rotate until required angle
rotation = ratio * ARROW_BOT_LINE_ANGLE;
} else {
// rotate back to start doing a 360
rotation = ARROW_BOT_LINE_ANGLE + (1 - ratio) * ARROW_TOP_LINE_ANGLE;
}
// rotate by middle
pivotX = width / 2;
pivotY = height / 2;
// shorten both ends
stopX -= resolveStrokeModifier(ratio);
startX += dip3 * ratio;
break;
}
iconPaint.setAlpha(alpha);
canvas.rotate(rotation, pivotX, pivotY);
canvas.rotate(rotation2, pivotX2, pivotY2);
canvas.drawLine(startX, startY, stopX, stopY, iconPaint);
iconPaint.setAlpha(255);
}
private void drawBottomLine(Canvas canvas, float ratio) {
canvas.restore();
canvas.save();
float rotation = 0, pivotX = 0, pivotY = 0;
float rotation2 = 0;
// pivot at center of line
float pivotX2 = width / 2 + dip3 / 2;
float pivotY2 = height - topPadding - dip2;
float startX = sidePadding;
float startY = height - topPadding - dip2;
float stopX = width - sidePadding;
float stopY = height - topPadding - dip2;
switch (animationState) {
case BURGER_ARROW:
if (isMorphingForward()) {
// rotate to required angle
rotation = ARROW_TOP_LINE_ANGLE * ratio;
} else {
// rotate back to start doing a 360
rotation = ARROW_TOP_LINE_ANGLE + (1 - ratio) * ARROW_BOT_LINE_ANGLE;
}
// pivot center of canvas
pivotX = width / 2;
pivotY = height / 2;
// shorten both ends
stopX = width - sidePadding - resolveStrokeModifier(ratio);
startX = sidePadding + dip3 * ratio;
break;
}
canvas.rotate(rotation, pivotX, pivotY);
canvas.rotate(rotation2, pivotX2, pivotY2);
canvas.drawLine(startX, startY, stopX, stopY, iconPaint);
}
private boolean isMorphingForward() {
return transformationValue <= TRANSFORMATION_MID;
}
private float resolveStrokeModifier(float ratio) {
switch (stroke) {
case REGULAR:
return ratio * dip3;
case THIN:
return ratio * (dip3 + diph);
case EXTRA_THIN:
return ratio * dip4;
}
return 0;
}
@Override public void setAlpha(int alpha) {
iconPaint.setAlpha(alpha);
}
@Override public void setColorFilter(ColorFilter cf) {
iconPaint.setColorFilter(cf);
}
@Override public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
/*
* Accessor methods
*/
public void setColor(int color) {
iconPaint.setColor(color);
circlePaint.setColor(color);
invalidateSelf();
}
public void setTransformationDuration(int duration) {
transformation.setDuration(duration);
}
public void setPressedDuration(int duration) {
pressedCircle.setDuration(duration);
}
public void setInterpolator(Interpolator interpolator) {
transformation.setInterpolator(interpolator);
}
public void setNeverDrawTouch(boolean neverDrawTouch) {
this.neverDrawTouch = neverDrawTouch;
}
public void setIconState(IconState iconState) {
synchronized (lock) {
if (transformationRunning) {
transformation.cancel();
transformationRunning = false;
}
if (currentIconState == iconState) return;
switch (iconState) {
case BURGER:
animationState = AnimationState.BURGER_ARROW;
transformationValue = TRANSFORMATION_START;
break;
case ARROW:
animationState = AnimationState.BURGER_ARROW;
transformationValue = TRANSFORMATION_MID;
break;
}
currentIconState = iconState;
invalidateSelf();
}
}
public void animateIconState(IconState state, boolean drawTouch) {
synchronized (lock) {
if (transformationRunning) {
transformation.end();
pressedCircle.end();
}
drawTouchCircle = drawTouch;
animatingIconState = state;
start();
}
}
public IconState setTransformationOffset(AnimationState animationState, float offset) {
if (offset < TRANSFORMATION_START || offset > TRANSFORMATION_END) {
throw new IllegalArgumentException(
String.format("Value must be between %s and %s", TRANSFORMATION_START, TRANSFORMATION_END)
);
}
this.animationState = animationState;
final boolean isFirstIcon = offset < TRANSFORMATION_MID || offset == TRANSFORMATION_END;
currentIconState = isFirstIcon ? animationState.getFirstState() : animationState.getSecondState();
animatingIconState = isFirstIcon ? animationState.getSecondState() : animationState.getFirstState();
setTransformationValue(offset);
return currentIconState;
}
public void setRTLEnabled(boolean rtlEnabled) {
this.rtlEnabled = rtlEnabled;
invalidateSelf();
}
public IconState getIconState() {
return currentIconState;
}
/*
* Animations
*/
private final Property<MaterialIndicatorDrawable, Float> transformationProperty
= new Property<MaterialIndicatorDrawable, Float>(Float.class, "transformation") {
@Override
public Float get(MaterialIndicatorDrawable object) {
return object.getTransformationValue();
}
@Override
public void set(MaterialIndicatorDrawable object, Float value) {
object.setTransformationValue(value);
}
};
private final Property<MaterialIndicatorDrawable, Float> pressedProgressProperty
= new Property<MaterialIndicatorDrawable, Float>(Float.class, "pressedProgress") {
@Override
public Float get(MaterialIndicatorDrawable object) {
return object.getPressedProgress();
}
@Override
public void set(MaterialIndicatorDrawable object, Float value) {
object.setPressedProgress(value);
}
};
public Float getTransformationValue() {
return transformationValue;
}
public void setTransformationValue(Float value) {
this.transformationValue = value;
invalidateSelf();
}
public Float getPressedProgress() {
return pressedProgressValue;
}
public void setPressedProgress(Float value) {
this.pressedProgressValue = value;
circlePaint.setAlpha((int) (DEFAULT_CIRCLE_ALPHA * (1 - value / (circleRadius * 1.22f))));
invalidateSelf();
}
private void initAnimations(int transformDuration, int pressedDuration) {
transformation = ObjectAnimator.ofFloat(this, transformationProperty, 0);
transformation.setInterpolator(new DecelerateInterpolator(3));
transformation.setDuration(transformDuration);
transformation.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
transformationRunning = false;
setIconState(animatingIconState);
}
});
pressedCircle = ObjectAnimator.ofFloat(this, pressedProgressProperty, 0, 0);
pressedCircle.setDuration(pressedDuration);
pressedCircle.setInterpolator(new DecelerateInterpolator());
pressedCircle.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
pressedProgressValue = 0;
}
@Override public void onAnimationCancel(Animator animation) {
pressedProgressValue = 0;
}
});
}
private boolean resolveTransformation() {
boolean isCurrentBurger = currentIconState == IconState.BURGER;
boolean isCurrentArrow = currentIconState == IconState.ARROW;
boolean isAnimatingBurger = animatingIconState == IconState.BURGER;
boolean isAnimatingArrow = animatingIconState == IconState.ARROW;
if ((isCurrentBurger && isAnimatingArrow) || (isCurrentArrow && isAnimatingBurger)) {
animationState = AnimationState.BURGER_ARROW;
return isCurrentBurger;
}
throw new IllegalStateException(
String.format("Animating from %s to %s is not supported", currentIconState, animatingIconState)
);
}
@Override public void start() {
if (transformationRunning || animatingIconState == null || animatingIconState == currentIconState) return;
transformationRunning = true;
final boolean direction = resolveTransformation();
transformation.setFloatValues(
direction ? TRANSFORMATION_START : TRANSFORMATION_MID,
direction ? TRANSFORMATION_MID : TRANSFORMATION_END
);
transformation.start();
if (pressedCircle.isRunning()) {
pressedCircle.cancel();
}
if (drawTouchCircle && !neverDrawTouch) {
pressedCircle.setFloatValues(0, circleRadius * 1.22f);
pressedCircle.start();
}
invalidateSelf();
}
@Override public void stop() {
if (isRunning() && transformation.isRunning()) {
transformation.end();
} else {
transformationRunning = false;
invalidateSelf();
}
}
@Override public boolean isRunning() {
return transformationRunning;
}
@Override
public int getIntrinsicWidth() {
return width;
}
@Override
public int getIntrinsicHeight() {
return height;
}
@Override
public ConstantState getConstantState() {
materialMenuState.changingConfigurations = getChangingConfigurations();
return materialMenuState;
}
@Override
public Drawable mutate() {
materialMenuState = new MaterialMenuState();
return this;
}
/**
* Call from {@link android.app.Activity#onSaveInstanceState(android.os.Bundle)} to store current icon state
*
* @param outState outState
*/
public void onSaveInstanceState(Bundle outState) {
outState.putString(STATE_KEY, currentIconState.name());
}
/**
* Call from {@link android.app.Activity#onPostCreate(android.os.Bundle)} to restore icon state
*
* @param state state
*/
public void syncState(Bundle state) {
if (state != null) {
String iconStateName = state.getString(STATE_KEY);
if (iconStateName == null) {
iconStateName = MaterialIndicatorDrawable.IconState.BURGER.name();
}
setIconState(MaterialIndicatorDrawable.IconState.valueOf(iconStateName));
}
}
private final class MaterialMenuState extends ConstantState {
private int changingConfigurations;
private MaterialMenuState() {
}
@Override
public Drawable newDrawable() {
MaterialIndicatorDrawable drawable = new MaterialIndicatorDrawable(
circlePaint.getColor(), stroke, transformation.getDuration(),
pressedCircle.getDuration(), width, height, iconWidth, circleRadius, strokeWidth, dip1
);
drawable.setIconState(animatingIconState != null ? animatingIconState : currentIconState);
drawable.setRTLEnabled(rtlEnabled);
return drawable;
}
@Override
public int getChangingConfigurations() {
return changingConfigurations;
}
}
static float dpToPx(Resources resources, float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics());
}
}
@@ -0,0 +1,446 @@
package com.hippo.ehviewer.drawable;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.annotation.TargetApi;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import java.util.ArrayList;
// Get from android.graphics.drawable.MaterialProgressDrawable
public class MaterialProgressDrawable extends Drawable implements Animatable {
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final TimeInterpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator();
private static final TimeInterpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator();
private static final long ANIMATION_DURATION = 1333;
private static final int NUM_POINTS = 5;
private final ArrayList<Animator> mAnimators = new ArrayList<Animator>();
private final Ring mRing;
private MaterialProgressState mState;
private float mRotation;
private boolean mMutated;
public MaterialProgressDrawable() {
this(new MaterialProgressState(null));
}
public MaterialProgressDrawable(int width, int height, int color, float strokeWidth, float innerRadius) {
this(new MaterialProgressState(width, height, color, strokeWidth, innerRadius));
}
private MaterialProgressDrawable(MaterialProgressState state) {
mState = state;
mRing = new Ring(mCallback);
mMutated = false;
initializeFromState();
setupAnimators();
}
private void initializeFromState() {
MaterialProgressState state = mState;
Ring ring = mRing;
ring.setStrokeWidth(state.mStrokeWidth);
int color = state.mColor.getColorForState(getState(), 0);
ring.setColor(color);
float minEdge = Math.min(state.mWidth, state.mHeight);
if ((state.mInnerRadius <= 0.0F) || (minEdge < 0.0F)) {
ring.setInsets((int) Math.ceil(state.mStrokeWidth / 2.0F));
} else {
float insets = minEdge / 2.0F - state.mInnerRadius;
ring.setInsets(insets);
}
}
@Override
public Drawable mutate() {
if ((!mMutated) && (super.mutate() == this)) {
mState = new MaterialProgressState(mState);
mMutated = true;
}
return this;
}
@Override
protected boolean onStateChange(int[] state) {
boolean changed = super.onStateChange(state);
int color = mState.mColor.getColorForState(state, 0);
if (color != mRing.getColor()) {
mRing.setColor(color);
changed = true;
}
return changed;
}
@Override
public boolean isStateful() {
return (super.isStateful()) || (mState.mColor.isStateful());
}
@Override
public int getIntrinsicHeight() {
return mState.mHeight;
}
@Override
public int getIntrinsicWidth() {
return mState.mWidth;
}
@Override
public void draw(Canvas c) {
Rect bounds = getBounds();
int saveCount = c.save();
c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
mRing.draw(c, bounds);
c.restoreToCount(saveCount);
}
public void setColor(int color) {
mState.mColor = ColorStateList.valueOf(color);
mRing.setColor(color);
}
@Override
public void setAlpha(int alpha) {
mRing.setAlpha(alpha);
}
@Override
public int getAlpha() {
return mRing.getAlpha();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mRing.setColorFilter(colorFilter);
}
private void setRotation(float rotation) {
mRotation = rotation;
invalidateSelf();
}
private float getRotation() {
return mRotation;
}
@Override
public int getOpacity() {
return -3;
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (visible) {
if ((changed) || (restart)) {
start();
}
} else {
stop();
}
return changed;
}
@Override
public boolean isRunning() {
ArrayList<Animator> animators = mAnimators;
int N = animators.size();
for (int i = 0; i < N; i++) {
Animator animator = animators.get(i);
if (animator.isRunning()) {
return true;
}
}
return false;
}
@Override
@TargetApi(Build.VERSION_CODES.KITKAT)
public void start() {
ArrayList<Animator> animators = mAnimators;
int N = animators.size();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (int i = 0; i < N; i++) {
Animator animator = animators.get(i);
if (animator.isPaused())
animator.resume();
else if (!animator.isRunning())
animator.start();
}
} else {
for (int i = 0; i < N; i++) {
Animator animator = animators.get(i);
if (!animator.isRunning())
animator.start();
}
}
}
@Override
@TargetApi(Build.VERSION_CODES.KITKAT)
public void stop() {
ArrayList<Animator> animators = mAnimators;
int N = animators.size();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (int i = 0; i < N; i++) {
animators.get(i).pause();
}
} else {
for (int i = 0; i < N; i++) {
animators.get(i).cancel();
}
}
}
private void setupAnimators() {
Ring ring = mRing;
ObjectAnimator endTrim = ObjectAnimator.ofFloat(ring, "endTrim",
new float[] { 0.0F, 0.75F });
endTrim.setDuration(ANIMATION_DURATION);
endTrim.setInterpolator(START_CURVE_INTERPOLATOR);
endTrim.setRepeatCount(-1);
endTrim.setRepeatMode(1);
ObjectAnimator startTrim = ObjectAnimator.ofFloat(ring, "startTrim",
new float[] { 0.0F, 0.75F });
startTrim.setDuration(ANIMATION_DURATION);
startTrim.setInterpolator(END_CURVE_INTERPOLATOR);
startTrim.setRepeatCount(-1);
startTrim.setRepeatMode(1);
ObjectAnimator rotation = ObjectAnimator.ofFloat(ring, "rotation",
new float[] { 0.0F, 0.25F });
rotation.setDuration(ANIMATION_DURATION);
rotation.setInterpolator(LINEAR_INTERPOLATOR);
rotation.setRepeatCount(-1);
rotation.setRepeatMode(1);
ObjectAnimator groupRotation = ObjectAnimator.ofFloat(this, "rotation",
new float[] { 0.0F, 360.0F });
groupRotation.setDuration(6665L);
groupRotation.setInterpolator(LINEAR_INTERPOLATOR);
groupRotation.setRepeatCount(-1);
groupRotation.setRepeatMode(1);
mAnimators.add(endTrim);
mAnimators.add(startTrim);
mAnimators.add(rotation);
mAnimators.add(groupRotation);
}
private final Drawable.Callback mCallback = new Drawable.Callback() {
@Override
public void invalidateDrawable(Drawable d) {
invalidateSelf();
}
@Override
public void scheduleDrawable(Drawable d, Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(Drawable d, Runnable what) {
unscheduleSelf(what);
}
};
private static class MaterialProgressState extends Drawable.ConstantState {
private float mStrokeWidth = 5.0F;
private float mInnerRadius = -1.0F;
private int mWidth = -1;
private int mHeight = -1;
private ColorStateList mColor = ColorStateList.valueOf(0);
public MaterialProgressState(int width, int height, int color, float strokeWidth, float innerRadius) {
mWidth = width;
mHeight = height;
mColor = ColorStateList.valueOf(color);
mStrokeWidth = strokeWidth;
mInnerRadius = innerRadius;
}
public MaterialProgressState(MaterialProgressState orig) {
if (orig != null) {
mStrokeWidth = orig.mStrokeWidth;
mInnerRadius = orig.mInnerRadius;
mWidth = orig.mWidth;
mHeight = orig.mHeight;
mColor = orig.mColor;
}
}
@Override
public int getChangingConfigurations() {
return 0;
}
@Override
public Drawable newDrawable() {
return new MaterialProgressDrawable(this);
}
}
private static class Ring {
private final RectF mTempBounds = new RectF();
private final Paint mPaint = new Paint();
private final Drawable.Callback mCallback;
private float mStartTrim = 0.0F;
private float mEndTrim = 0.0F;
private float mRotation = 0.0F;
private float mStrokeWidth = 5.0F;
private float mStrokeInset = 2.5F;
private int mAlpha = 255;
private int mColor = -16777216;
public Ring(Drawable.Callback callback) {
mCallback = callback;
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
}
public void draw(Canvas c, Rect bounds) {
RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
arcBounds.inset(mStrokeInset, mStrokeInset);
float startAngle = (mStartTrim + mRotation) * 360.0F;
float endAngle = (mEndTrim + mRotation) * 360.0F;
float sweepAngle = endAngle - startAngle;
float diameter = Math.min(arcBounds.width(), arcBounds.height());
float minAngle = (float) (360.0D / (diameter * 3.141592653589793D));
if ((sweepAngle < minAngle) && (sweepAngle > -minAngle)) {
sweepAngle = Math.signum(sweepAngle) * minAngle;
}
c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
}
public void setColorFilter(ColorFilter filter) {
mPaint.setColorFilter(filter);
invalidateSelf();
}
public ColorFilter getColorFilter() {
return mPaint.getColorFilter();
}
public void setAlpha(int alpha) {
mAlpha = alpha;
mPaint.setColor(mColor & 0xFFFFFF | alpha << 24);
invalidateSelf();
}
public int getAlpha() {
return mAlpha;
}
public void setColor(int color) {
mColor = color;
mPaint.setColor(color & 0xFFFFFF | mAlpha << 24);
invalidateSelf();
}
public int getColor() {
return mColor;
}
public void setStrokeWidth(float strokeWidth) {
mStrokeWidth = strokeWidth;
mPaint.setStrokeWidth(strokeWidth);
invalidateSelf();
}
public float getStrokeWidth() {
return mStrokeWidth;
}
public void setStartTrim(float startTrim) {
mStartTrim = startTrim;
invalidateSelf();
}
public float getStartTrim() {
return mStartTrim;
}
public void setEndTrim(float endTrim) {
mEndTrim = endTrim;
invalidateSelf();
}
public float getEndTrim() {
return mEndTrim;
}
public void setRotation(float rotation) {
mRotation = rotation;
invalidateSelf();
}
public float getRotation() {
return mRotation;
}
public void setInsets(float insets) {
mStrokeInset = insets;
}
public float getInsets() {
return mStrokeInset;
}
private void invalidateSelf() {
mCallback.invalidateDrawable(null);
}
}
private static class EndCurveInterpolator extends
AccelerateDecelerateInterpolator {
@Override
public float getInterpolation(float input) {
return super
.getInterpolation(Math.max(0.0F, (input - 0.5F) * 2.0F));
}
}
private static class StartCurveInterpolator extends
AccelerateDecelerateInterpolator {
@Override
public float getInterpolation(float input) {
return super.getInterpolation(Math.min(1.0F, input * 2.0F));
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.drawable;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
public class OvalDrawable extends Drawable {
private final Paint mPaint;
private final RectF mRectF;
public OvalDrawable(int color) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(color);
mRectF = new RectF();
}
@Override
protected void onBoundsChange(Rect bounds) {
mRectF.set(bounds);
}
@Override
public void draw(Canvas canvas) {
canvas.drawOval(mRectF, mPaint);
}
@Override
public void setAlpha(int alpha) {
mPaint.setColor(mPaint.getColor() & 0x00ffffff | (alpha << 24));
invalidateSelf();
}
@Override
public void setColorFilter(ColorFilter cf) {
// Not support
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
public void setColor(int color) {
mPaint.setColor(color);
invalidateSelf();
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.effect;
import android.animation.ValueAnimator;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.Drawable;
public class DrawableTransition {
public static void transit(final Drawable drawable, boolean fromDark, long duration) {
final AlphaSatColorMatrixEvaluator evaluator = new AlphaSatColorMatrixEvaluator(fromDark);
evaluator.setFraction(0.0f);
drawable.setColorFilter(new ColorMatrixColorFilter(evaluator.getColorMatrix()));
ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float f = (Float) animation.getAnimatedValue();
evaluator.setFraction(f);
drawable.setColorFilter(new ColorMatrixColorFilter(evaluator.getColorMatrix()));
}
});
animator.setDuration(duration);
animator.start();
}
// From rnr.tests.imageloadingpattern.AlphaSatColorMatrixEvaluator
// with modification
private static class AlphaSatColorMatrixEvaluator {
private final ColorMatrix colorMatrix;
private final float[] elements = new float[20];
private final boolean mFromDark;
public AlphaSatColorMatrixEvaluator(boolean fromDark) {
colorMatrix = new ColorMatrix ();
mFromDark = fromDark;
}
public ColorMatrix getColorMatrix() {
return colorMatrix;
}
public void setFraction(float fraction) {
// There are 4 phases so we multiply fraction by that amount
float phase = fraction * 4;
// Compute the alpha change over period [0, 2]
float alpha = Math.min(phase, 2.0f) / 2.0f;
elements [19] = Math.round(alpha * 255);
// We substract to make the picture look darker, it will automatically clamp
// This is spread over period [0, 3]
final int MaxBlacker = 50;
float blackening = Math.round((1 - Math.min(phase, 3.0f) / 3.0f) * MaxBlacker);
elements [4] = elements [9] = elements [14] = mFromDark ? -blackening : blackening;
// Finally we desaturate over [0, 4], taken from ColorMatrix.SetSaturation
float invSat = 1 - Math.max(0.2f, fraction);
float R = 0.213f * invSat;
float G = 0.715f * invSat;
float B = 0.072f * invSat;
elements[0] = R + fraction; elements[1] = G; elements[2] = B;
elements[5] = R; elements[6] = G + fraction; elements[7] = B;
elements[10] = R; elements[11] = G; elements[12] = B + fraction;
colorMatrix.set(elements);
}
}
}
@@ -0,0 +1,423 @@
/*
* Copyright (C) 2013 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.hippo.ehviewer.effect.ripple;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.os.Build;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import com.hippo.ehviewer.util.MathUtils;
/**
* Draws a Material ripple.
*/
class Ripple {
@SuppressWarnings("unused")
private static final String TAG = Ripple.class.getSimpleName();
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final Interpolator DECEL_INTERPOLATOR = new LogInterpolator();
private static final float GLOBAL_SPEED = 1.0f;
private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
private static final long RIPPLE_ENTER_DELAY = 80;
private final RippleOwner mOwner;
/** Bounds used for computing max radius. */
private final Rect mBounds;
/** Full-opacity color for drawing this ripple. */
private int mColorOpaque;
/** Maximum ripple radius. */
private float mOuterRadius;
/** Screen density used to adjust pixel-based velocities. */
private float mDensity;
private float mStartingX;
private float mStartingY;
private float mClampedStartingX;
private float mClampedStartingY;
// Software animators.
private ObjectAnimator mAnimRadius;
private ObjectAnimator mAnimOpacity;
private ObjectAnimator mAnimX;
private ObjectAnimator mAnimY;
// Software rendering properties.
private float mOpacity = 1;
private float mOuterX;
private float mOuterY;
// Values used to tween between the start and end positions.
private float mTweenRadius = 0;
private float mTweenX = 0;
private float mTweenY = 0;
/** Whether we have an explicit maximum radius. */
private boolean mHasMaxRadius;
/** Whether we were canceled externally and should avoid self-removal. */
private boolean mCanceled;
/**
* Creates a new ripple.
*/
public Ripple(RippleOwner owner, Rect bounds, float startingX, float startingY) {
mOwner = owner;
mBounds = bounds;
mStartingX = startingX;
mStartingY = startingY;
}
public void setup(int maxRadius, int color, float density) {
mColorOpaque = color | 0xFF000000;
mOpacity = Color.alpha(color) / 2 / 255.0f;
if (maxRadius != RippleOwner.RADIUS_AUTO) {
mHasMaxRadius = true;
mOuterRadius = maxRadius;
} else {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
}
mOuterX = 0;
mOuterY = 0;
mDensity = density;
clampStartingPosition();
}
private void clampStartingPosition() {
final float cX = mBounds.exactCenterX();
final float cY = mBounds.exactCenterY();
final float dX = mStartingX - cX;
final float dY = mStartingY - cY;
final float r = mOuterRadius;
if (dX * dX + dY * dY > r * r) {
// Point is outside the circle, clamp to the circumference.
final double angle = Math.atan2(dY, dX);
mClampedStartingX = cX + (float) (Math.cos(angle) * r);
mClampedStartingY = cY + (float) (Math.sin(angle) * r);
} else {
mClampedStartingX = mStartingX;
mClampedStartingY = mStartingY;
}
}
public void onHotspotBoundsChanged() {
if (!mHasMaxRadius) {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
clampStartingPosition();
}
}
public void setOpacity(float a) {
mOpacity = a;
invalidateSelf();
}
public float getOpacity() {
return mOpacity;
}
public void setRadiusGravity(float r) {
mTweenRadius = r;
invalidateSelf();
}
public float getRadiusGravity() {
return mTweenRadius;
}
public void setXGravity(float x) {
mTweenX = x;
invalidateSelf();
}
public float getXGravity() {
return mTweenX;
}
public void setYGravity(float y) {
mTweenY = y;
invalidateSelf();
}
public float getYGravity() {
return mTweenY;
}
/**
* Draws the ripple centered at (0,0) using the specified paint.
*/
public boolean draw(Canvas c, Paint p) {
return drawSoftware(c, p);
}
private boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
p.setColor(mColorOpaque);
final int alpha = MathUtils.clamp((int) (255 * mOpacity + 0.5f), 0, 255);
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
if (alpha > 0 && radius > 0) {
final float x = MathUtils.lerp(
mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
final float y = MathUtils.lerp(
mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
p.setAlpha(alpha);
p.setStyle(Style.FILL);
c.drawCircle(x, y, radius, p);
hasContent = true;
}
return hasContent;
}
/**
* Returns the maximum bounds of the ripple relative to the ripple center.
*/
public void getBounds(Rect bounds) {
final int outerX = (int) mOuterX;
final int outerY = (int) mOuterY;
final int r = (int) mOuterRadius + 1;
bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
}
/**
* Specifies the starting position relative to the drawable bounds. No-op if
* the ripple has already entered.
*/
public void move(float x, float y) {
mStartingX = x;
mStartingY = y;
clampStartingPosition();
}
/**
* Starts the enter animation.
*/
public void enter() {
cancel();
final int radiusDuration = (int)
(1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
radius.setDuration(radiusDuration);
radius.setInterpolator(LINEAR_INTERPOLATOR);
radius.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
cX.setDuration(radiusDuration);
cX.setInterpolator(LINEAR_INTERPOLATOR);
cX.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
cY.setDuration(radiusDuration);
cY.setInterpolator(LINEAR_INTERPOLATOR);
cY.setStartDelay(RIPPLE_ENTER_DELAY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
radius.setAutoCancel(true);
cX.setAutoCancel(true);
cY.setAutoCancel(true);
}
mAnimRadius = radius;
mAnimX = cX;
mAnimY = cY;
// Enter animations always run on the UI thread, since it's unlikely
// that anything interesting is happening until the user lifts their
// finger.
radius.start();
cX.start();
cY.start();
}
/**
* Starts the exit animation.
*/
public void exit() {
cancel();
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
final float remaining;
if (mAnimRadius != null && mAnimRadius.isRunning()) {
remaining = mOuterRadius - radius;
} else {
remaining = mOuterRadius;
}
final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
+ WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
final int opacityDuration = (int) (1000 * 1 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
exitSoftware(radiusDuration, opacityDuration);
}
/**
* Jump all animations to their end state. The caller is responsible for
* removing the ripple from the list of animating ripples.
*/
public void jump() {
mCanceled = true;
endSoftwareAnimations();
mCanceled = false;
}
private void endSoftwareAnimations() {
if (mAnimRadius != null) {
mAnimRadius.end();
mAnimRadius = null;
}
if (mAnimOpacity != null) {
mAnimOpacity.end();
mAnimOpacity = null;
}
if (mAnimX != null) {
mAnimX.end();
mAnimX = null;
}
if (mAnimY != null) {
mAnimY.end();
mAnimY = null;
}
}
private void exitSoftware(int radiusDuration, int opacityDuration) {
final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
radiusAnim.setDuration(radiusDuration);
radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
xAnim.setDuration(radiusDuration);
xAnim.setInterpolator(DECEL_INTERPOLATOR);
final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
yAnim.setDuration(radiusDuration);
yAnim.setInterpolator(DECEL_INTERPOLATOR);
final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
opacityAnim.setDuration(opacityDuration);
opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
opacityAnim.addListener(mAnimationListener);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
radiusAnim.setAutoCancel(true);
xAnim.setAutoCancel(true);
yAnim.setAutoCancel(true);
opacityAnim.setAutoCancel(true);
}
mAnimRadius = radiusAnim;
mAnimOpacity = opacityAnim;
mAnimX = xAnim;
mAnimY = yAnim;
radiusAnim.start();
opacityAnim.start();
xAnim.start();
yAnim.start();
}
/**
* Cancels all animations. The caller is responsible for removing
* the ripple from the list of animating ripples.
*/
public void cancel() {
mCanceled = true;
cancelSoftwareAnimations();
mCanceled = false;
}
private void cancelSoftwareAnimations() {
if (mAnimRadius != null) {
mAnimRadius.cancel();
mAnimRadius = null;
}
if (mAnimOpacity != null) {
mAnimOpacity.cancel();
mAnimOpacity = null;
}
if (mAnimX != null) {
mAnimX.cancel();
mAnimX = null;
}
if (mAnimY != null) {
mAnimY.cancel();
mAnimY = null;
}
}
private void removeSelf() {
// The owner will invalidate itself.
if (!mCanceled) {
mOwner.removeRipple(this);
}
}
private void invalidateSelf() {
mOwner.invalidateSelf();
}
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
removeSelf();
}
};
/**
* Interpolator with a smooth log deceleration
*/
private static final class LogInterpolator implements Interpolator {
@Override
public float getInterpolation(float input) {
return 1 - (float) Math.pow(400, -input * 1.4);
}
}
}
@@ -0,0 +1,285 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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
* 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.hippo.ehviewer.effect.ripple;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.os.Build;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import com.hippo.ehviewer.util.MathUtils;
/**
* Draws a Material ripple.
*/
class RippleBackground {
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final float GLOBAL_SPEED = 1.0f;
private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
private static final float WAVE_OUTER_OPACITY_ENTER_VELOCITY = 10.0f * GLOBAL_SPEED;
private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
private final RippleOwner mOwner;
/** Bounds used for computing max radius. */
private final Rect mBounds;
/** Full-opacity color for drawing this ripple. */
private int mColorOpaque;
/** Maximum alpha value for drawing this ripple. */
private int mColorAlpha;
/** Maximum ripple radius. */
private float mOuterRadius;
/** Screen density used to adjust pixel-based velocities. */
private float mDensity;
// Software animators.
private ObjectAnimator mAnimOuterOpacity;
// Software rendering properties.
private float mOuterOpacity = 0;
private float mOuterX;
private float mOuterY;
/** Whether we have an explicit maximum radius. */
private boolean mHasMaxRadius;
/**
* Creates a new ripple.
*/
public RippleBackground(RippleOwner owner, Rect bounds) {
mOwner = owner;
mBounds = bounds;
}
public void setup(int maxRadius, int color, float density) {
mColorOpaque = color | 0xFF000000;
mColorAlpha = Color.alpha(color) / 2;
if (maxRadius != RippleOwner.RADIUS_AUTO) {
mHasMaxRadius = true;
mOuterRadius = maxRadius;
} else {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
}
mOuterX = 0;
mOuterY = 0;
mDensity = density;
}
public void onHotspotBoundsChanged() {
if (!mHasMaxRadius) {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
}
}
public void setOuterOpacity(float a) {
mOuterOpacity = a;
invalidateSelf();
}
public float getOuterOpacity() {
return mOuterOpacity;
}
/**
* Draws the ripple centered at (0,0) using the specified paint.
*/
public boolean draw(Canvas c, Paint p) {
return drawSoftware(c, p);
}
public boolean shouldDraw() {
final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f);
return outerAlpha > 0 && mOuterRadius > 0;
}
private boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
p.setColor(mColorOpaque);
final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f);
if (outerAlpha > 0 && mOuterRadius > 0) {
p.setAlpha(outerAlpha);
p.setStyle(Style.FILL);
c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
hasContent = true;
}
return hasContent;
}
/**
* Returns the maximum bounds of the ripple relative to the ripple center.
*/
public void getBounds(Rect bounds) {
final int outerX = (int) mOuterX;
final int outerY = (int) mOuterY;
final int r = (int) mOuterRadius + 1;
bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
}
/**
* Starts the enter animation.
*/
public void enter() {
cancel();
final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_ENTER_VELOCITY);
final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
outer.setDuration(outerDuration);
outer.setInterpolator(LINEAR_INTERPOLATOR);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outer.setAutoCancel(true);
}
mAnimOuterOpacity = outer;
// Enter animations always run on the UI thread, since it's unlikely
// that anything interesting is happening until the user lifts their
// finger.
outer.start();
}
/**
* Starts the exit animation.
*/
public void exit() {
cancel();
// Scale the outer max opacity and opacity velocity based
// on the size of the outer radius.
final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
final float outerSizeInfluence = MathUtils.clamp(
(mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
/ (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
// Determine at what time the inner and outer opacity intersect.
// inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
// outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
/ (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
final int inflectionOpacity = (int) (mColorAlpha * (mOuterOpacity
+ inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
}
/**
* Jump all animations to their end state. The caller is responsible for
* removing the ripple from the list of animating ripples.
*/
public void jump() {
endSoftwareAnimations();
}
private void endSoftwareAnimations() {
if (mAnimOuterOpacity != null) {
mAnimOuterOpacity.end();
mAnimOuterOpacity = null;
}
}
private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
final ObjectAnimator outerOpacityAnim;
if (inflectionDuration > 0) {
// Outer opacity continues to increase for a bit.
outerOpacityAnim = ObjectAnimator.ofFloat(this,
"outerOpacity", inflectionOpacity / 255.0f);
outerOpacityAnim.setDuration(inflectionDuration);
outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outerOpacityAnim.setAutoCancel(true);
}
// Chain the outer opacity exit animation.
final int outerDuration = opacityDuration - inflectionDuration;
if (outerDuration > 0) {
outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
RippleBackground.this, "outerOpacity", 0);
outerFadeOutAnim.setDuration(outerDuration);
outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outerFadeOutAnim.setAutoCancel(true);
}
mAnimOuterOpacity = outerFadeOutAnim;
outerFadeOutAnim.start();
}
@Override
public void onAnimationCancel(Animator animation) {
animation.removeListener(this);
}
});
}
} else {
outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
outerOpacityAnim.setDuration(opacityDuration);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outerOpacityAnim.setAutoCancel(true);
}
}
mAnimOuterOpacity = outerOpacityAnim;
outerOpacityAnim.start();
}
/**
* Cancel all animations. The caller is responsible for removing
* the ripple from the list of animating ripples.
*/
public void cancel() {
cancelSoftwareAnimations();
}
private void cancelSoftwareAnimations() {
if (mAnimOuterOpacity != null) {
mAnimOuterOpacity.cancel();
mAnimOuterOpacity = null;
}
}
private void invalidateSelf() {
mOwner.invalidateSelf();
}
}
@@ -0,0 +1,579 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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
* 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.hippo.ehviewer.effect.ripple;
import android.annotation.TargetApi;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import com.hippo.ehviewer.widget.Hotspotable;
import java.util.Arrays;
/**
* Drawable that shows a ripple effect in response to state changes. The
* anchoring position of the ripple for a given state may be specified by
* calling {@link #setHotspot(float, float)} with the corresponding state
* attribute identifier.
* <p>
* A touch feedback drawable may contain multiple child layers, including a
* special mask layer that is not drawn to the screen. A single layer may be set
* as the mask by specifying its android:id value as {@link android.R.id#mask}.
* <pre>
* <code>&lt!-- A red ripple masked against an opaque rectangle. --/>
* &ltripple android:color="#ffff0000">
* &ltitem android:id="@android:id/mask"
* android:drawable="@android:color/white" />
* &ltripple /></code>
* </pre>
* <p>
* If a mask layer is set, the ripple effect will be masked against that layer
* before it is drawn over the composite of the remaining child layers.
* <p>
* If no mask layer is set, the ripple effect is masked against the composite
* of the child layers.
* <pre>
* <code>&lt!-- A blue ripple drawn atop a black rectangle. --/>
* &ltripple android:color="#ff00ff00">
* &ltitem android:drawable="@android:color/black" />
* &ltripple />
*
* &lt!-- A red ripple drawn atop a drawable resource. --/>
* &ltripple android:color="#ff00ff00">
* &ltitem android:drawable="@drawable/my_drawable" />
* &ltripple /></code>
* </pre>
* <p>
* If no child layers or mask is specified and the ripple is set as a View
* background, the ripple will be drawn atop the first available parent
* background within the View's hierarchy. In this case, the drawing region
* may extend outside of the Drawable bounds.
* <pre>
* <code>&lt!-- An unbounded green ripple. --/>
* &ltripple android:color="#ff0000ff" /></code>
* </pre>
*
* @attr ref android.R.styleable#RippleDrawable_color
*/
public class RippleDrawable extends Drawable implements RippleOwner, Hotspotable {
@SuppressWarnings("unused")
private static final String TAG = RippleDrawable.class.getSimpleName();
/** The maximum number of ripples supported. */
private static final int MAX_RIPPLES = 10;
/** Current ripple effect bounds, used to constrain ripple effects. */
private final Rect mHotspotBounds = new Rect();
/** The current background. May be actively animating or pending entry. */
private RippleBackground mBackground;
/** Whether we expect to draw a background when visible. */
private boolean mBackgroundActive;
/** The current ripple. May be actively animating or pending entry. */
private Ripple mRipple;
/** Whether we expect to draw a ripple when visible. */
private boolean mRippleActive;
// Hotspot coordinates that are awaiting activation.
private float mPendingX;
private float mPendingY;
private boolean mHasPending;
/**
* Lazily-created array of actively animating ripples. Inactive ripples are
* pruned during draw(). The locations of these will not change.
*/
private Ripple[] mExitingRipples;
private int mExitingRipplesCount = 0;
/** Paint used to control appearance of ripples. */
private Paint mRipplePaint;
/** Target density of the display into which ripples are drawn. */
private final float mDensity = 1.0f;
/** Whether bounds are being overridden. */
private boolean mOverrideBounds;
/**
* Whether the next draw MUST draw something to canvas. Used to work around
* a bug in hardware invalidation following a render thread-accelerated
* animation.
*/
private boolean mNeedsDraw;
private ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
private int mMaxRadius = RADIUS_AUTO;
private final Drawable mContent;
RippleDrawable(ColorStateList color, Drawable content) {
setColor(color);
mContent = content;
}
@Override
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void jumpToCurrentState() {
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
super.jumpToCurrentState();
}
boolean needsDraw = false;
if (mRipple != null) {
mRipple.jump();
}
if (mBackground != null) {
mBackground.jump();
}
needsDraw |= cancelExitingRipples();
mNeedsDraw = needsDraw;
invalidateSelf();
}
private boolean cancelExitingRipples() {
final int count = mExitingRipplesCount;
final Ripple[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].cancel();
}
if (ripples != null) {
Arrays.fill(ripples, 0, count, null);
}
mExitingRipplesCount = 0;
return false;
}
@Override
public void setAlpha(int alpha) {
mColor.withAlpha(alpha);
}
@Override
public int getAlpha() {
return Color.alpha(mColor.getDefaultColor());
}
@Override
public void setColorFilter(ColorFilter cf) {
//TODO Paint.setColorFilter(filter) ?
}
@Override
public int getOpacity() {
// Worst-case scenario.
return PixelFormat.TRANSLUCENT;
}
@Override
protected boolean onStateChange(int[] stateSet) {
boolean changed = false;
if (mContent != null)
changed |= mContent.setState(stateSet);
boolean enabled = false;
boolean pressed = false;
boolean focused = false;
for (int state : stateSet) {
if (state == android.R.attr.state_enabled) {
enabled = true;
}
if (state == android.R.attr.state_focused) {
focused = true;
}
if (state == android.R.attr.state_pressed) {
pressed = true;
}
}
setRippleActive(enabled && pressed);
setBackgroundActive(focused || (enabled && pressed));
return changed;
}
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
tryRippleEnter();
} else {
tryRippleExit();
}
}
}
private void setBackgroundActive(boolean active) {
if (mBackgroundActive != active) {
mBackgroundActive = active;
if (active) {
tryBackgroundEnter();
} else {
tryBackgroundExit();
}
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (!mOverrideBounds) {
mHotspotBounds.set(bounds);
onHotspotBoundsChanged();
}
if (mContent != null)
mContent.setBounds(bounds);
invalidateSelf();
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
final boolean changed = super.setVisible(visible, restart);
if (!visible) {
clearHotspots();
} else if (changed) {
// If we just became visible, ensure the background and ripple
// visibilities are consistent with their internal states.
if (mRippleActive) {
tryRippleEnter();
}
if (mBackgroundActive) {
tryBackgroundEnter();
}
}
return changed;
}
@Override
public boolean isStateful() {
return true;
}
public void setColor(ColorStateList color) {
int alpha = Color.alpha(color.getDefaultColor());
mColor = color.withAlpha((int) (alpha * (1 - 0.001f * alpha)));
invalidateSelf();
}
@Override
public void setHotspot(float x, float y) {
if (mRipple == null || mBackground == null) {
mPendingX = x;
mPendingY = y;
mHasPending = true;
}
if (mRipple != null) {
mRipple.move(x, y);
}
}
/**
* Creates an active hotspot at the specified location.
*/
private void tryBackgroundEnter() {
if (mBackground == null) {
mBackground = new RippleBackground(this, mHotspotBounds);
}
final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
mBackground.setup(mMaxRadius, color, mDensity);
mBackground.enter();
}
private void tryBackgroundExit() {
if (mBackground != null) {
// Don't null out the background, we need it to draw!
mBackground.exit();
}
}
/**
* Attempts to start an enter animation for the active hotspot. Fails if
* there are too many animating ripples.
*/
private void tryRippleEnter() {
if (mExitingRipplesCount >= MAX_RIPPLES) {
// This should never happen unless the user is tapping like a maniac
// or there is a bug that's preventing ripples from being removed.
return;
}
if (mRipple == null) {
final float x;
final float y;
if (mHasPending) {
mHasPending = false;
x = mPendingX;
y = mPendingY;
} else {
x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY();
}
mRipple = new Ripple(this, mHotspotBounds, x, y);
}
final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
mRipple.setup(mMaxRadius, color, mDensity);
mRipple.enter();
}
/**
* Attempts to start an exit animation for the active hotspot. Fails if
* there is no active hotspot.
*/
private void tryRippleExit() {
if (mRipple != null) {
if (mExitingRipples == null) {
mExitingRipples = new Ripple[MAX_RIPPLES];
}
mExitingRipples[mExitingRipplesCount++] = mRipple;
mRipple.exit();
mRipple = null;
}
}
/**
* Cancels and removes the active ripple, all exiting ripples, and the
* background. Nothing will be drawn after this method is called.
*/
private void clearHotspots() {
boolean needsDraw = false;
if (mRipple != null) {
mRipple.cancel();
mRipple = null;
}
if (mBackground != null) {
mBackground.cancel();
mBackground = null;
}
needsDraw |= cancelExitingRipples();
mNeedsDraw = needsDraw;
invalidateSelf();
}
@Override
public void setHotspotBounds(int left, int top, int right, int bottom) {
mOverrideBounds = true;
mHotspotBounds.set(left, top, right, bottom);
onHotspotBoundsChanged();
}
/** @hide */
public void getHotspotBounds(Rect outRect) {
outRect.set(mHotspotBounds);
}
/**
* Notifies all the animating ripples that the hotspot bounds have changed.
*/
private void onHotspotBoundsChanged() {
final int count = mExitingRipplesCount;
final Ripple[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].onHotspotBoundsChanged();
}
if (mRipple != null) {
mRipple.onHotspotBoundsChanged();
}
if (mBackground != null) {
mBackground.onHotspotBoundsChanged();
}
}
@Override
public void draw(@NonNull Canvas canvas) {
final boolean drawNonMaskContent = mContent != null;
final Rect bounds = getDirtyBounds();
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(bounds);
// If we have content, draw it into a layer first.
if (drawNonMaskContent) {
drawContentLayer(canvas, bounds);
}
// If we have a background and a non-opaque mask, draw the masking layer.
drawBackgroundLayer(canvas, bounds);
// If we have ripples and a non-opaque mask, draw the masking layer.
drawRippleLayer(canvas, bounds);
// If we failed to draw anything and we just canceled animations, at
// least draw a color so that hardware invalidation works correctly.
if (mNeedsDraw) {
canvas.drawColor(Color.TRANSPARENT);
// Request another draw so we can avoid adding a transparent layer
// during the next display list refresh.
invalidateSelf();
}
mNeedsDraw = false;
canvas.restoreToCount(saveCount);
}
/**
* Removes a ripple from the exiting ripple list.
*
* @param ripple the ripple to remove
*/
@Override
public void removeRipple(Ripple ripple) {
// Ripple ripple ripple ripple. Ripple ripple.
final Ripple[] ripples = mExitingRipples;
final int count = mExitingRipplesCount;
final int index = getRippleIndex(ripple);
if (index >= 0) {
System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
ripples[count - 1] = null;
mExitingRipplesCount--;
invalidateSelf();
}
}
private int getRippleIndex(Ripple ripple) {
final Ripple[] ripples = mExitingRipples;
final int count = mExitingRipplesCount;
for (int i = 0; i < count; i++) {
if (ripples[i] == ripple) {
return i;
}
}
return -1;
}
private void drawContentLayer(Canvas canvas, Rect bounds) {
mContent.draw(canvas);
}
private void drawBackgroundLayer(
Canvas canvas, Rect bounds) {
if (mBackground != null && mBackground.shouldDraw()) {
final float x = mHotspotBounds.exactCenterX();
final float y = mHotspotBounds.exactCenterY();
canvas.translate(x, y);
mBackground.draw(canvas, getRipplePaint());
canvas.translate(-x, -y);
}
}
private void drawRippleLayer(Canvas canvas, Rect bounds) {
int restoreTranslate = -1;
// Translate the canvas to the current hotspot bounds.
restoreTranslate = canvas.save();
canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
// Draw ripples and update the animating ripples array.
final int count = mExitingRipplesCount;
final Ripple[] ripples = mExitingRipples;
for (int i = 0; i <= count; i++) {
final Ripple ripple;
if (i < count) {
ripple = ripples[i];
} else if (mRipple != null) {
ripple = mRipple;
} else {
continue;
}
ripple.draw(canvas, getRipplePaint());
}
// Always restore the translation.
if (restoreTranslate >= 0) {
canvas.restoreToCount(restoreTranslate);
}
}
private Paint getRipplePaint() {
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
}
return mRipplePaint;
}
@Override
public Rect getDirtyBounds() {
return getBounds();
}
/**
* Sets the maximum ripple radius in pixels. The default value of
* {@link #RADIUS_AUTO} defines the radius as the distance from the center
* of the drawable bounds (or hotspot bounds, if specified) to a corner.
*
* @param maxRadius the maximum ripple radius in pixels or
* {@link #RADIUS_AUTO} to automatically determine the maximum
* radius based on the bounds
* @see #getMaxRadius()
* @see #setHotspotBounds(int, int, int, int)
* @hide
*/
public void setMaxRadius(int maxRadius) {
if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
}
mMaxRadius = maxRadius;
}
/**
* @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
* the radius is determined automatically
* @see #setMaxRadius(int)
* @hide
*/
public int getMaxRadius() {
return mMaxRadius;
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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
* 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.hippo.ehviewer.effect.ripple;
public interface RippleOwner {
/**
* Constant for automatically determining the maximum ripple radius.
*/
public static final int RADIUS_AUTO = -1;
public void removeRipple(Ripple ripple);
public void invalidateSelf();
}
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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.hippo.ehviewer.effect.ripple;
import android.annotation.TargetApi;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
import com.hippo.ehviewer.widget.HotspotTouchHelper;
public final class RippleSalon {
private static final boolean USE_OLD_RIPPLE =
Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
private static final Drawable MASK = new ColorDrawable(Color.BLACK);
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void addRipple(View v, ColorStateList color) {
addRipple(v, color, v.getBackground());
}
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void addRipple(View v, ColorStateList color, Drawable content) {
if (USE_OLD_RIPPLE) {
RippleDrawable rippleDrawable = new RippleDrawable(color, content);
v.setOnTouchListener(new HotspotTouchHelper(rippleDrawable));
v.setBackgroundDrawable(rippleDrawable);
} else {
android.graphics.drawable.RippleDrawable rippleDrawable = new android.graphics.drawable.RippleDrawable(color, content, MASK);
v.setBackgroundDrawable(rippleDrawable);
}
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
public class AddToFavoriteParser {
public boolean parser(String body) {
if (body == null)
return false;
if (body.contains("Close Window"))
return true;
else
return false;
}
}
@@ -0,0 +1,324 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hippo.ehviewer.data.Comment;
import com.hippo.ehviewer.data.LargePreviewList;
import com.hippo.ehviewer.data.NormalPreviewList;
import com.hippo.ehviewer.data.PreviewList;
import com.hippo.ehviewer.util.EhUtils;
import com.hippo.ehviewer.util.Utils;
public class DetailParser {
private static final String OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>";
private static final String PINING_STRING =
"<p>This gallery is pining for the fjords.</p>";
public static final int DETAIL = 0x1;
public static final int TAG = 0x2;
public static final int PREVIEW_INFO = 0x4;
public static final int PREVIEW = 0x8;
public static final int COMMENT = 0x10;
public static final int OFFENSIVE = 0x20;
public static final int PINING = 0x40;
public static final int ERROR = 0x80;
private static final CommentSort cs = new CommentSort();
public String eMesg;
public String thumb;
public String title;
public String title_jpn;
public int category;
public String uploader;
public String posted;
public int pages;
public String size;
public String resized;
public String parent;
public String visible;
public String language;
public int people;
public float rating;
public String firstPage;
public int previewPerPage;
public int previewSum;
public LinkedHashMap<String, LinkedList<String>> tags;
public PreviewList previewList;
public LinkedList<Comment> comments;
public void reset() {
eMesg = null;
thumb = null;
title = null;
title_jpn = null;
category = 0;
uploader = null;
posted = null;
pages = 0;
size = null;
resized = null;
parent = null;
visible = null;
language = null;
people = 0;
rating = 0;
firstPage = null;
previewPerPage = 0;
previewSum = 0;
tags = null;
previewList = null;
comments = null;
}
public int parser(String body, int mode) {
int re = 0;
Pattern p;
Matcher m;
if (body == null)
return 0;
if (!body.contains("<")) {
eMesg = body;
return ERROR;
}
if (body.contains(OFFENSIVE_STRING)) {
return OFFENSIVE;
}
if (body.contains(PINING_STRING)) {
return PINING;
}
p = Pattern.compile("<div class=\"d\">\n<p>([^<]+)</p>");
m = p.matcher(body);
if (m.find()) {
eMesg = m.group(1);
return ERROR;
}
// Get detail
if ((mode & DETAIL) != 0) {
p = Pattern
.compile("<div id=\"gd1\"><img src=\"([^\"]+)\"[^<>]+/></div>" // thumb
+ "</div>"
+ "<div id=\"gd2\">"
+ "<h1 id=\"gn\">([^<>]+)</h1>" // title
+ "<h1 id=\"gj\">([^<>]*)</h1>" // title_jpn might be empty string
+ "</div>"
+ ".+"
+ "<div id=\"gdc\"><a[^<>]+><[^<>]*alt=\"([\\w|\\-]+)\"[^<>]*/></a></div>" // category
+ "<div id=\"gdn\"><a[^<>]+>([^<>]+)</a>" // uploader
+ ".+"
+ "<tr><td[^<>]*>Posted:</td><td[^<>]*>([\\w|\\-|\\s|:]+)</td></tr>" // posted
+ "<tr><td[^<>]*>Images:</td><td[^<>]*>([\\d]+) @ ([\\w|\\.|\\s]+)</td></tr>" // pages and size
+ "<tr><td[^<>]*>Resized:</td><td[^<>]*>([^<>]+)</td></tr>" // resized
+ "<tr><td[^<>]*>Parent:</td><td[^<>]*>(?:<a[^<>]*>)?([^<>]+)(?:</a>)?</td></tr>" // parent
+ "<tr><td[^<>]*>Visible:</td><td[^<>]*>([^<>]+)</td></tr>" // visible
+ "<tr><td[^<>]*>Language:</td><td[^<>]*>([^<>]+)</td></tr>" // language
+ ".+"
+ "<td id=\"grt3\">\\(<span id=\"rating_count\">([\\d|,]+)</span>\\)</td>" // people
+ "</tr>"
+ "<tr><td[^<>]*>([^<>]+)</td>" // rating
+ ".+"
+ "<div id=\"gdt\"><div[^<>]*>(?:<div[^<>]*>)?<a[^<>]*href=\"([^<>\"]+)\"[^<>]*>"); // get firstPage
m = p.matcher(body);
if (m.find()) {
re |= DETAIL;
thumb = Utils.unescapeXml(m.group(1));
title = Utils.unescapeXml(m.group(2));
title_jpn = Utils.unescapeXml(m.group(3));
category = EhUtils.getCategory(m.group(4));
uploader = m.group(5);
posted = m.group(6);
pages = Integer.parseInt(m.group(7));
size = m.group(8);
resized = m.group(9);
parent = m.group(10);
visible = m.group(11);
language = m.group(12);
people = Integer.parseInt(m.group(13).replace(",", ""));
Pattern pattern = Pattern.compile("([\\d|\\.]+)");
Matcher matcher = pattern.matcher(m.group(14));
if (matcher.find())
rating = Float.parseFloat(matcher.group(1));
else
rating = Float.NaN;
firstPage = m.group(15);
}
}
// Get tag
if ((mode & TAG) != 0) {
tags = new LinkedHashMap<String, LinkedList<String>>();
p = Pattern
.compile("<tr><td[^<>]+>([\\w\\s]+):</td><td>(?:<div[^<>]+><a[^<>]+>[\\w\\s]+</a></div>)+</td></tr>");
m = p.matcher(body);
while (m.find()) {
re |= TAG;
String groupName = m.group(1);
LinkedList<String> group = getTagGroup(m.group(0));
if (group != null) {
tags.put(groupName, group);
}
}
}
// Get preview info
if ((mode & PREVIEW_INFO) != 0) {
p = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>");
m = p.matcher(body);
if (m.find()) {
re |= PREVIEW_INFO;
previewSum = Integer.valueOf(m.group(1).replace(",", ""));
}
}
// Get preview
if ((mode & PREVIEW) != 0) {
boolean isLargePreview = false;
if (body.contains("<div class=\"gdtl\""))
isLargePreview = true;
if (isLargePreview) {
previewList = new LargePreviewList();
p = Pattern.compile("<div class=\"gdtl\".+?<a href=\"(.+?)\"><img.+?src=\"(.+?)\"");
m = p.matcher(body);
while (m.find()) {
re |= PREVIEW;
((LargePreviewList)previewList).addItem(m.group(2), m.group(1));
}
} else {
previewList = new NormalPreviewList();
p = Pattern.compile("<div[^<>]*class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*>");
m = p.matcher(body);
while (m.find()) {
re |= PREVIEW;
((NormalPreviewList)previewList).addItem(m.group(3), m.group(4), "0", m.group(1),
m.group(2), m.group(5));
}
}
// Set previewPerPage
if (previewList != null)
previewPerPage = previewList.size();
}
// Get comment
if ((mode & COMMENT) != 0) {
p = Pattern
.compile("<div class=\"c3\">Posted on ([^<>]+) by <a[^<>]+>([^<>]+)</a>.+?<div class=\"c6\"[^>]*>(.+?)</div><div class=\"c[78]\"");
m = p.matcher(body);
comments = new LinkedList<Comment>();
while (m.find()) {
re |= COMMENT;
comments.add(new Comment(m.group(1), m.group(2), m.group(3)));
}
Collections.sort(comments, cs);
}
return re;
}
private LinkedList<String> getTagGroup(String pageContent) {
LinkedList<String> list = new LinkedList<String>();
Pattern p = Pattern.compile("<div[^<>]+><a[^<>]+>([\\w\\s]+)</a></div>");
Matcher m = p.matcher(pageContent);
while (m.find())
list.add(m.group(1));
if (list.size() == 0)
return null;
else
return list;
}
static class CommentSort implements Comparator<Comment> {
private int compareNum(String n1, String n2, int median) {
int re = 0;
for (int i = 0; i < median; i++) {
re *= 10;
re += n1.charAt(i) - n2.charAt(i);
}
return re;
}
private int getMonth(String m) {
if (m.equals("January"))
return 1;
else if (m.equals("February"))
return 2;
else if (m.equals("March"))
return 3;
else if (m.equals("April"))
return 4;
else if (m.equals("May"))
return 5;
else if (m.equals("June"))
return 6;
else if (m.equals("July"))
return 7;
else if (m.equals("August"))
return 8;
else if (m.equals("September"))
return 9;
else if (m.equals("October"))
return 10;
else if (m.equals("November"))
return 11;
else if (m.equals("December"))
return 12;
else
return 0;
}
@Override
public int compare(Comment c1, Comment c2) {
int re = 0;
Pattern p = Pattern.compile("(\\d{2}) (\\w+) (\\d{4}), (\\d{2}):(\\d{2})");
Matcher m1 = p.matcher(c1.time);
Matcher m2 = p.matcher(c2.time);
if (!m1.find() || !m2.find())
return 0;
// year
re += compareNum(m1.group(3), m2.group(3), 4);
re <<= 4;
// Month
re += getMonth(m1.group(2)) - getMonth(m2.group(2));
re <<= 5;
// Day
re += compareNum(m1.group(1), m2.group(1), 2);
re <<= 5;
// Hour
re += compareNum(m1.group(4), m2.group(4), 2);
re <<= 6;
// Minute
re += compareNum(m1.group(5), m2.group(5), 2);
return re;
}
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DetailUrlParser {
public int gid;
public String token;
public boolean parser(String url) {
if (url == null)
return false;
Pattern p = Pattern.compile("/(\\d+)/(\\w+)");
Matcher m = p.matcher(url);
if (m.find()) {
gid = Integer.parseInt(m.group(1));
token = m.group(2);
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,131 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hippo.ehviewer.util.Log;
public class EdDetailParser {
int previewPageNum;
int previewStartIndex;
int imageNum;
boolean isLastPage;
List<String> pageTokenArray;
String emsg;
public boolean parser(String body, int mode, boolean needPreviewInfo) {
Pattern p;
Matcher m;
if (body == null)
return false;
if (!body.contains("<"))
emsg = body;
if (mode == EhClient.MODE_LOFI) {
// Check is last page
p = Pattern.compile("<a[^>]+>Next Page &gt;</a>");
m = p.matcher(body);
if (m.find())
isLastPage = false;
else
isLastPage = true;
// Parser preview
p = Pattern.compile("<div class=\"gi\"[^>]*><a href=\"http://lofi.e-hentai.org/s/(\\w+)/\\d+-(\\d+)\"[^>]*>");
m = p.matcher(body);
boolean first = true;
while(m.find()) {
if (first) {
pageTokenArray = new LinkedList<String>();
previewStartIndex = Integer.parseInt(m.group(2)) - 1;
first = false;
}
pageTokenArray.add(m.group(1));
}
if (first)
return false;
else
return true;
} else {
// Parser preview info
if (needPreviewInfo) {
p = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>");
m = p.matcher(body);
if (m.find())
previewPageNum = Integer.valueOf(m.group(1).replace(",", ""));
else {
Log.e("TAG", "get needPreviewInfo failed");
return false;
}
p = Pattern.compile("<td class=\"gdt1\">Images:</td><td class=\"gdt2\">([\\d,]+) ");
m = p.matcher(body);
if (m.find())
imageNum = Integer.valueOf(m.group(1).replace(",", ""));
else {
Log.e("TAG", "get needPreviewInfo failed");
return false;
}
}
// Parser preview
boolean first = true;
if (body.contains("<div class=\"gdtl\"")) {
p = Pattern.compile("<div class=\"gdtl\"[^>]*><a href=\"http://[^/]+/s/(\\w+)/\\d+-(\\d+)\">");
m = p.matcher(body);
while(m.find()) {
if (first) {
pageTokenArray = new LinkedList<String>();
previewStartIndex = Integer.parseInt(m.group(2)) - 1;
first = false;
}
pageTokenArray.add(m.group(1));
}
} else {
p = Pattern.compile("<div class=\"gdtm\"[^>]*><div[^>]*><a href=\"http://[^/]+/s/(\\w+)/\\d+-(\\d+)\">");
m = p.matcher(body);
while(m.find()) {
if (first) {
pageTokenArray = new LinkedList<String>();
previewStartIndex = Integer.parseInt(m.group(2)) - 1;
first = false;
}
pageTokenArray.add(m.group(1));
}
}
if (first) {
Log.e("TAG", "get gdtm");
return false;
} else
return true;
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,267 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Utils;
import com.larvalabs.svgandroid.SVGBuilder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.List;
/**
*
* @author Hippo
*
*/
public class EhInfo {
private static final String TAG = EhInfo.class.getSimpleName();
private static final String PREF_NAME = "eh_info";
private static final String DIR_NAME = "file";
private static final String AVATAR_NAME = "avatar.png";
private static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.PNG;
private static Drawable DEFAULT_AVATAR;
public static final String EX_HOST = "exhentai.org";
public static final String[] COOKIABLE_HOSTS = {"exhentai.org",
"g.e-hentai.org", "forums.e-hentai.org", "ul.exhentai.org",
"ul.e-hentai.org", "lofi.e-hentai.org"};
private static final String KEY_LOGIN = "login";
private static final boolean DEFAULT_LOGIN = false;
private static final String KEY_USERNAME = "username";
private static final String KEY_DISPLAYNAME = "displayname";
private static final String DEFAULT_NAME = "Hippo";
private static final String KEY_MEMBER_ID = "ipb_member_id";
private static final String DEFAULT_MEMBER_ID = null;
private static final String KEY_PASS_HASH = "ipb_pass_hash";
private static final String DEFAULT_PASS_HASH = null;
private final Context mContext;
private final SharedPreferences mInfoPref;
private String mUconfig;
private boolean mIsLogin;
private String mUsername;
private String mDisplayname;
private BitmapDrawable mAvatar;
private int mDefaultCat;
private String mPreviewMode;
private int mExculdeTagGroup;
private String mExculdeLanguage;
private static EhInfo sInstance;
private BitmapDrawable getAvatarFromFile() {
File dir = mContext.getDir(DIR_NAME, 0);
File avatarFile = new File(dir, AVATAR_NAME);
if (!avatarFile.exists())
return null;
InputStream is = null;
try {
is = new FileInputStream(avatarFile);
return (BitmapDrawable) BitmapDrawable.createFromStream(is, null);
} catch (FileNotFoundException e) {
return null;
} finally {
Utils.closeQuietly(is);
}
}
private EhInfo(final Context context){
mContext = context;
mInfoPref = mContext.getSharedPreferences(PREF_NAME, 0);
if (DEFAULT_AVATAR == null)
DEFAULT_AVATAR = new SVGBuilder().readFromResource(context.getResources(), R.raw.default_avatar).build().getDrawable();
mIsLogin = mInfoPref.getBoolean(KEY_LOGIN, DEFAULT_LOGIN);
mUsername = mInfoPref.getString(KEY_USERNAME, DEFAULT_NAME);
mDisplayname = mInfoPref.getString(KEY_DISPLAYNAME, DEFAULT_NAME);
mAvatar = getAvatarFromFile();
mDefaultCat = Config.getDefaultCat();
mPreviewMode = Config.getPreviewMode();
mExculdeTagGroup = Config.getExculdeTagGroup();
mExculdeLanguage = Config.getExculdeLanguage();
updateUconfig();
};
private String getUconfigString(String previewMode) {
return new StringBuilder().append("cats_").append(mDefaultCat)
.append("-ts_").append(previewMode == null ? mPreviewMode : previewMode)
.append("-xns_").append(mExculdeTagGroup)
.append("-xl_").append(mExculdeLanguage)
.append("-tl_m-uh_y-tr_2-prn_n-dm_l-ar_0-rc_0-rx_0-ry_0-sa_y-oi_n-qb_n-tf_n-hp_-hk_-ms_n-mt_n")
.toString();
}
private void updateUconfig() {
mUconfig = getUconfigString(null);
}
public final static EhInfo getInstance(final Context context) {
if (sInstance == null)
sInstance = new EhInfo(context.getApplicationContext());
return sInstance;
}
private String getCookie(String cookieStr, String key) {
String value = null;
int index1 = -1;
int index2 = -1;
if ((index1 = cookieStr.indexOf(key + "=")) != -1) {
index1 += key.length() + 1; // size of key + "="
if ((index2 = cookieStr.indexOf(";", index1)) != -1)
value = cookieStr.substring(index1, index2);
else
value = cookieStr.substring(index1);
}
return value;
}
public void storeCookie(HttpURLConnection conn) {
String ipb_member_id;
String ipb_pass_hash;
List<String> cookieList = conn.getHeaderFields().get("Set-Cookie");
if (cookieList == null)
return;
SharedPreferences.Editor editor = mInfoPref.edit();
for (String str : cookieList) {
ipb_member_id = getCookie(str, KEY_MEMBER_ID);
ipb_pass_hash = getCookie(str, KEY_PASS_HASH);
if (ipb_member_id != null)
editor.putString(KEY_MEMBER_ID, ipb_member_id);
if (ipb_pass_hash != null)
editor.putString(KEY_PASS_HASH, ipb_pass_hash);
}
editor.apply();
}
public void setCookie(HttpURLConnection conn) {
setCookie(conn, null);
}
public void setCookie(HttpURLConnection conn, String previewMode) {
StringBuilder sb = new StringBuilder();
String id = mInfoPref.getString(KEY_MEMBER_ID, DEFAULT_MEMBER_ID);
String hash = mInfoPref.getString(KEY_PASS_HASH, DEFAULT_PASS_HASH);
sb.append("uconfig=").append(previewMode == null ? mUconfig : getUconfigString(previewMode))
.append("; xres=").append(Config.getLofiResolution());
if (id != null)
sb.append("; ipb_member_id=").append(id);
if (hash != null)
sb.append("; ipb_pass_hash=").append(hash);
// Fake
sb.append("; igneous=");
sb.append("; lv=");
conn.setRequestProperty("Cookie", sb.toString());
}
public boolean isLogin() {
return mIsLogin;
}
public void login(String username, String displayname) {
mIsLogin = true;
mUsername = username;
mDisplayname = displayname;
mInfoPref.edit().putBoolean(KEY_LOGIN, true)
.putString(KEY_USERNAME, username)
.putString(KEY_DISPLAYNAME, displayname).apply();
}
public void logout() {
mIsLogin = false;
// Remove avatar
if (mAvatar != null) {
mAvatar.getBitmap().recycle();
mAvatar = null;
File dir = mContext.getDir(DIR_NAME, 0);
File avatarFile = new File(dir, AVATAR_NAME);
avatarFile.delete();
}
mInfoPref.edit().putBoolean(KEY_LOGIN, false)
.putString(KEY_MEMBER_ID, DEFAULT_MEMBER_ID)
.putString(KEY_PASS_HASH, DEFAULT_PASS_HASH).apply();
}
public String getUsername() {
return mUsername;
}
public String getDisplayname() {
return mDisplayname;
}
public Drawable getAvatar() {
return isLogin() && mAvatar != null ? mAvatar : DEFAULT_AVATAR;
}
public void setAvatar(BitmapDrawable avatar) {
mAvatar = avatar;
File dir = mContext.getDir(DIR_NAME, 0);
File avatarFile = new File(dir, AVATAR_NAME);
OutputStream os = null;
try {
os = new FileOutputStream(avatarFile);
avatar.getBitmap().compress(AVATAR_FORMAT, 100, os);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
Utils.closeQuietly(os);
}
}
public void setDefaultCat(int defaultCat) {
mDefaultCat = defaultCat;
updateUconfig();
}
public void setPreviewMode(String previewMode) {
mPreviewMode = previewMode;
updateUconfig();
}
public void setExculdeTagGroup(int exculdeTagGroup) {
mExculdeTagGroup = exculdeTagGroup;
updateUconfig();
}
public void setExculdeLanguage(String exculdeLanguage) {
mExculdeLanguage = exculdeLanguage;
updateUconfig();
}
}
@@ -0,0 +1,922 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Process;
import android.support.v4.app.NotificationCompat;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.network.HttpHelper;
import com.hippo.ehviewer.util.AutoExpandArray;
import com.hippo.ehviewer.util.BgThread;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.EhUtils;
import com.hippo.ehviewer.util.Log;
import com.hippo.ehviewer.util.Utils;
// TODO what a mess
public final class ExDownloader implements Runnable {
private static final String TAG = ExDownloader.class.getSimpleName();
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final String[] URL_ARRAY_509 = new String[] {
"http://ehgt.org/g/509.gif",
"http://ehgt.org/g/509s.gif",
"http://st.exhentai.net/img/509.gif",
"http://st.exhentai.net/img/509s.gif"
};
private final Context mContext;
private final ExDownloaderManager mManager;
private final int mGid;
private final String mToken;
private final String mTitle;
private final int mMode;
private volatile int mPreviewPageNum = -1;
private volatile int mPreviewPerPage = -1;
private volatile int mImageNum = -1;
private final AutoExpandArray<String> mPageTokeArray = new AutoExpandArray<String>();
private final AutoExpandArray<String> mImageFilenameArray = new AutoExpandArray<String>();
private volatile int mOwnerNum = 0;
private Thread mMainThread = null;
/** Only work for EhClient.MODE_LOFI when do not know image number **/
private int mCurMaxPreviewPage = -1;
private volatile int mStartIndex = 0;
private final Queue<Integer> mRequestIndexArray = new ConcurrentLinkedQueue<Integer>();
private final Queue<Integer> mNoTokenIndexArray = new ConcurrentLinkedQueue<Integer>();
private final Queue<Integer> mRequestPageIndexArray = new ConcurrentLinkedQueue<Integer>();
/** The set contains the requsting index **/
private final Set<Integer> mRequstingIndexSet = new HashSet<Integer>();
private volatile int mCurRequestPageIndex = -1;
private final int mWorkerNum;
private final Worker[] mWorkerArray;
private final HttpHelper.DownloadControlor[] mControlorArray;
private final Object mPageTokenLock = new Object();
private final Object mWorkerLock = new Object();
private final Object mWorkerGetIndexLock = new Object();
private volatile boolean mDownloadMode = false;
private volatile boolean mPauseWork = false;
private volatile boolean mStopWork = false;
private final File mDir;
private ListenerForImageSet mLfis;
private ListenerForDownload mLfd;
private static final long UPDATE_SPEED_INTERVAL = 500;
private volatile long mLastUpdateSpeedTime = 0;
private volatile long mAccumulateSize;
private volatile HashSet<Integer> mDownloadIndexSet;
private volatile Object mDownloadLock = new Object();
private int mCurReadIndex = 0;
/**
* Do no work in UI thread
* @author Hippo
*/
public interface ListenerForImageSet {
public void onDownloadStart(int index);
public void onDownloading(int index, float percent);
public void onDownloadComplete(int index);
public void onDownloadFail(int index);
}
/**
* Do no work in UI thread
* @author Hippo
*/
public interface ListenerForDownload {
public void onStart(int gid);
public void onGetSum(int gid, int sum);
public void onDownload(int gid, int downloadSize, int totalSize);
public void onUpdateSpeed(int gid, int speed);
public void onDownloadOver(int gid, int legacy);
}
ExDownloader(Context context, int gid, String token, String title, int mode) {
mContext = context;
mManager = ExDownloaderManager.getInstance();
mGid = gid;
mToken = token;
mTitle = title;
mMode = mode;
mWorkerNum = Config.getDownloadThread();
mWorkerArray = new Worker[mWorkerNum];
mControlorArray = new HttpHelper.DownloadControlor[mWorkerNum];
// Make sure dir
mDir = EhUtils.getGalleryDir(mGid, mTitle);
Utils.ensureDir(mDir, true);
}
public void setListenerForImageSet(ListenerForImageSet l) {
mLfis = l;
}
public void setListenerDownload(ListenerForDownload l) {
mLfd = l;
}
void occupy() {
mOwnerNum++;
}
void free() {
mOwnerNum--;
}
boolean isOrphans() {
return mOwnerNum == 0;
}
void stop() {
// TODO stop all thread, release all resources
mStopWork = true;
// Stop download
for (HttpHelper.DownloadControlor c : mControlorArray) {
if (c != null)
c.stop();
}
// Wake all thread
synchronized(mPageTokenLock) {mPageTokenLock.notifyAll();}
synchronized(mWorkerGetIndexLock) {mWorkerGetIndexLock.notifyAll();}
synchronized(mWorkerLock) {mWorkerLock.notifyAll();}
}
boolean pause(boolean pause) {
if (mOwnerNum > 1)
return false;
mPauseWork = pause;
if (mPauseWork) {
// Stop download
for (HttpHelper.DownloadControlor c : mControlorArray)
if (c != null) c.stop();
// Wake all thread
synchronized(mPageTokenLock) {mPageTokenLock.notifyAll();}
synchronized(mWorkerGetIndexLock) {mWorkerGetIndexLock.notifyAll();}
synchronized(mWorkerLock) {mWorkerLock.notifyAll();}
} else {
// Reset download
for (HttpHelper.DownloadControlor c : mControlorArray)
if (c != null) c.reset();
ensureStart();
ensureWorkers();
}
return true;
}
public int getGid() {
return mGid;
}
public String getToken() {
return mToken;
}
public String getTitle() {
return mTitle;
}
public void setDownloadMode(boolean downloadMode) {
synchronized (mDownloadLock) {
if (downloadMode) {
setStartIndex(0);
if (mDownloadIndexSet == null)
mDownloadIndexSet = new HashSet<Integer>();
if (mDownloadMode && mLfd != null)
mLfd.onStart(mGid);
} else {
mDownloadIndexSet = null;
}
mDownloadMode = downloadMode;
}
}
public void setStartIndex(int startIndex) {
if (mDownloadMode) {
addTargetIndex(startIndex);
} else {
if (mImageNum != -1 && startIndex >= mImageNum)
mStartIndex = 0;
else
mStartIndex = startIndex;
ensureStart();
ensureWorkers();
}
}
public void addTargetIndex(int index) {
if (mImageNum != -1 && index >= mImageNum)
return;
mRequestIndexArray.offer(index);
ensureStart();
ensureWorkers();
}
public void setCurReadIndex(final int index) {
new BgThread() {
@Override
public void run() {
writeCurReadIndex(index);
}
}.start();
}
public int getMaxEnsureIndex() {
if (mImageNum != -1) {
return mImageNum - 1;
} else {
return Math.max(Math.max(Math.max(mStartIndex,
Math.max(mCurMaxPreviewPage, mPreviewPageNum - 1) * mPreviewPerPage),
mPageTokeArray.maxValidIndex()), 0);
}
}
/**
* If do not know image num, return -1.
* You should call getMaxEnsureIndex().
*
* @return
*/
public int getImageNum() {
return mImageNum;
}
public AutoExpandArray<String> getImageFilenameArray() {
return mImageFilenameArray;
}
/**
* If targer index image is downloading, return true
*
* @param index
* @return
*/
public boolean isDownloading(int index) {
return mRequstingIndexSet.contains(index);
}
private synchronized void ensureStart() {
if (mMainThread != null)
return;
mMainThread = new BgThread(this);
mMainThread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);
mMainThread.start();
}
private File getExDownloadInfoFile() {
return new File(mDir, EhUtils.EH_DOWNLOAD_FILENAME);
}
private boolean checkMode(int samlpe, int target) {
return samlpe <= EhClient.MODE_EX && target <= EhClient.MODE_EX
|| samlpe == EhClient.MODE_LOFI && target == EhClient.MODE_LOFI;
}
/**
* This function parser the download info file.<br>
* The file look like this:<br>
* <code>
* 00000000<br>
* 728874<br>
* 306429c222<br>
* 1<br>
* 4<br>
* 40<br>
* 128<br>
* 1 43a64e6e79<br>
* </code><br>
* Fist line is gid, a integer.
* Second line is token, a ten-character string.
* Third line is mode.
* Fourth line is preview page num.
* Fifth line is preview per page.
*
* @param ediFile
* @return
*/
private synchronized boolean parserEdiFile(File ediFile) {
if (!ediFile.exists() || !ediFile.isFile() || !ediFile.canRead()
|| !ediFile.canWrite())
return false;
InputStream is = null;
try {
is = new BufferedInputStream(new FileInputStream(ediFile),
IO_BUFFER_SIZE);
// skip read index
Utils.readAsciiLine(is);
if (mGid != Integer.valueOf(Utils.readAsciiLine(is)) ||
!mToken.equals(Utils.readAsciiLine(is)))
return false;
if (!checkMode(Integer.valueOf(Utils.readAsciiLine(is)), mMode)) {
// If preview info is not same
// skip preview page num and preview per page
Utils.readAsciiLine(is);
Utils.readAsciiLine(is);
} else {
mPreviewPageNum = Integer.valueOf(Utils.readAsciiLine(is));
mPreviewPerPage = Integer.valueOf(Utils.readAsciiLine(is));
}
mImageNum = Integer.parseInt(Utils.readAsciiLine(is));
if (mImageNum != -1) {
mPageTokeArray.setCapacity(mImageNum);
mImageFilenameArray.setCapacity(mImageNum);
}
else if (mPreviewPageNum != -1 && mPreviewPerPage != -1) {
mPageTokeArray.setCapacity(mPreviewPageNum * mPreviewPerPage);
mImageFilenameArray.setCapacity(mPreviewPageNum * mPreviewPerPage);
}
// read page token info
try {
while(true) {
String line = Utils.readAsciiLine(is);
int pos = line.indexOf(" ");
if (pos == -1 && line.length() - pos != 11)
continue;
try {
int index = Integer.parseInt(line.substring(0, pos));
String token = line.substring(pos + 1);
mPageTokeArray.set(index, token);
} catch (NumberFormatException e) {
continue;
}
}
} catch (EOFException e) {
// Empty
}
return true;
} catch (Throwable e) {
// If read error
return false;
} finally {
Utils.closeQuietly(is);
}
}
private synchronized void writeEdiFile(File ediFile) {
try {
FileWriter writer = new FileWriter(ediFile);
writer.write(String.format("%08x", mCurReadIndex));
writer.write("\n");
writer.write(String.valueOf(mGid));
writer.write("\n");
writer.write(mToken);
writer.write("\n");
writer.write(mMode + '0');
writer.write("\n");
writer.write(String.valueOf(mPreviewPageNum));
writer.write("\n");
writer.write(String.valueOf(mPreviewPerPage));
writer.write("\n");
writer.write(String.valueOf(mImageNum));
writer.write("\n");
for (int i = 0; i < mPageTokeArray.length(); i++) {
String pageToken = mPageTokeArray.get(i);
if (pageToken != null) {
writer.write(String.valueOf(i));
writer.write(" ");
writer.write(pageToken);
writer.write("\n");
}
}
writer.flush();
writer.close();
} catch (IOException e) {}
}
private synchronized void writeCurReadIndex(int index) {
mCurReadIndex = index;
try {
RandomAccessFile raf = new RandomAccessFile(getExDownloadInfoFile(), "rw");
raf.write(String.format("%08x", mCurReadIndex).getBytes());
raf.close();
} catch (Throwable e) {}
}
public static int readCurReadIndex(int gid, String title) {
try {
RandomAccessFile raf = new RandomAccessFile(new File(EhUtils.getGalleryDir(gid, title),
EhUtils.EH_DOWNLOAD_FILENAME), "r");
int index = Integer.parseInt(raf.readLine(), 16);
raf.close();
return index;
} catch (Throwable e) {
return 0;
}
}
private void getDetailInfo(int pageIndex, File ediFile, boolean needPreviewInfo) throws Exception {
HttpHelper hh = new HttpHelper(mContext);
EdDetailParser edp = new EdDetailParser();
String url = EhClient.getDetailUrl(mGid, mToken, pageIndex, mMode);
hh.setPreviewMode("m");
String body = hh.get(url);
if (body == null)
throw new Exception(hh.getEMsg() != null ? hh.getEMsg() : "Http error, body == null");
if (!edp.parser(body, mMode, needPreviewInfo))
throw new Exception(edp.emsg != null ? edp.emsg : "Parser error, body is \n" + body);
if (edp.previewStartIndex != pageIndex * mPreviewPerPage)
throw new Exception("预测与实际不匹配, 预测: " + edp.previewStartIndex +
", 实际: " + pageIndex * mPreviewPerPage + ", body is \n" + body);
List<String> pageTokenArray = edp.pageTokenArray;
if (needPreviewInfo)
mPreviewPerPage = pageTokenArray.size();
if (mMode == EhClient.MODE_LOFI) {
// update mCurMaxPreviewPage
if (edp.isLastPage) {
mPreviewPageNum = pageIndex + 1;
mImageNum = pageIndex * mPreviewPerPage + pageTokenArray.size();
} else if (!edp.isLastPage && mPreviewPageNum == -1) {
mCurMaxPreviewPage = Math.max(mCurMaxPreviewPage, pageIndex + 1);
}
} else {
if (needPreviewInfo) {
mPreviewPageNum = edp.previewPageNum;
mImageNum = edp.imageNum;
mPageTokeArray.setCapacity(mPreviewPageNum * mPreviewPerPage);
mImageFilenameArray.setCapacity(mPreviewPageNum * mPreviewPerPage);
}
}
for (int i = 0; i < pageTokenArray.size(); i++)
mPageTokeArray.set(i + edp.previewStartIndex, pageTokenArray.get(i));
writeEdiFile(ediFile);
}
private void ensureWorkers() {
if (mPreviewPerPage == -1)
return;
synchronized(mWorkerArray) {
for (int i = 0; i < mWorkerArray.length; i++) {
if (mWorkerArray[i] == null) {
Worker worker = new Worker(i);
worker.start();
mWorkerArray[i] = worker;
mControlorArray[i] = new HttpHelper.DownloadControlor();
}
}
}
}
private void updateDownload(int index) {
synchronized (mDownloadLock) {
if (mDownloadIndexSet != null && !mDownloadIndexSet.contains(index)) {
mDownloadIndexSet.add(index);
if (mDownloadMode && mLfd != null)
mLfd.onDownload(mGid, mDownloadIndexSet.size(), getMaxEnsureIndex() + 1);
}
}
}
private static final int BE_NOTIFY_ID = -3;
private static final void show509Notification(Context context) {
NotificationManager notifyManager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context);
mBuilder.setSmallIcon(R.drawable.ic_stat_eh).setContentTitle(context.getString(R.string.notification_be_title))
.setContentText(context.getString(R.string.notification_be_mesg))
.setOngoing(false).setAutoCancel(true);
notifyManager.notify(BE_NOTIFY_ID, mBuilder.build());
}
// TODO for download , when to claim download completed
@Override
public void run() {
// Try to get info from file
try {
File ediFile = getExDownloadInfoFile();
if (!parserEdiFile(ediFile) || mPreviewPerPage == -1 ||
((mMode == EhClient.MODE_EX || mMode == EhClient.MODE_G) && mImageNum == -1)) {
getDetailInfo(0, ediFile, true);
}
if (mLfd != null)
mLfd.onGetSum(mGid, mImageNum);
ensureWorkers();
// A loop to get page token
while (true) {
// Check stop
if (mStopWork)
break;
int noTokenIndex = -1;
int pageIndex = -1;
synchronized(mPageTokenLock) {
if (mNoTokenIndexArray.isEmpty()) {
if (mRequestPageIndexArray.isEmpty()) {
try {
mPageTokenLock.wait();
} catch (InterruptedException e) {}
continue;
} else {
pageIndex = mRequestPageIndexArray.poll();
mCurRequestPageIndex = pageIndex;
}
} else {
noTokenIndex = mNoTokenIndexArray.poll();
}
}
if (noTokenIndex != -1) {
if (mPageTokeArray.get(noTokenIndex) == null) {
getDetailInfo(noTokenIndex / mPreviewPerPage, ediFile, false);
} else {
// The token is already got, no need to re
}
} else if (noTokenIndex == -1){
getDetailInfo(pageIndex, ediFile, false);
mCurRequestPageIndex = -1;
}
ensureWorkers();
synchronized(mWorkerGetIndexLock) {mWorkerGetIndexLock.notifyAll();}
synchronized(mWorkerLock) {mWorkerLock.notifyAll();}
}
} catch (Exception e) {
Log.f(e);
synchronized (mDownloadLock) {
if (mDownloadMode && mLfd != null && mDownloadIndexSet != null) {
mLfd.onDownloadOver(mGid,
getMaxEnsureIndex() + 1 - mDownloadIndexSet.size());
}
}
} finally {
synchronized (this) {
mMainThread = null;
}
}
Log.d(TAG, "ExDownloader over");
}
private class Worker extends BgThread {
private final int mIndex;
public Worker(int index) {
super();
mIndex = index;
setPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
private void addNoTokenIndex(int index) {
synchronized(mPageTokenLock) {
mNoTokenIndexArray.offer(index);
mPageTokenLock.notify();
}
}
private void addRequstPageIndex(int pageIndex) {
synchronized(mPageTokenLock) {
if (mCurRequestPageIndex != pageIndex &&
!mRequestPageIndexArray.contains(pageIndex)) {
mRequestPageIndexArray.offer(pageIndex);
mPageTokenLock.notify();
}
}
}
private int getTargetIndex() {
synchronized(mWorkerGetIndexLock) {
while (true) {
// Check stop
if (mStopWork || mPauseWork)
return -1;
if (!mRequestIndexArray.isEmpty()) {
return mRequestIndexArray.poll();
} else if (mImageNum != -1 && mStartIndex == mImageNum) {
return -1;
} else if (mImageNum == -1 && mStartIndex > getMaxEnsureIndex()) {
// If do not sure the index is valid
Log.d(TAG, "mPreviewPerPage = " + mPreviewPerPage);
addRequstPageIndex(getMaxEnsureIndex() / mPreviewPerPage);
try {
mWorkerGetIndexLock.wait();
} catch (InterruptedException e) {}
} else {
return mStartIndex++;
}
}
}
}
// TODO what if download failed ? what to do ?
@Override
public void run() {
HttpDownloadListener listener = new HttpDownloadListener();
HttpHelper hh = new HttpHelper(mContext);
ImagePageParser ipp = new ImagePageParser();
while (true) {
// Check stop
if (mStopWork || mPauseWork)
break;
int targetIndex = getTargetIndex();
if (targetIndex == -1)
break;
// Check is the index is requsted
if (mRequstingIndexSet.contains(targetIndex))
continue;
// Check is this already downloaded
boolean isAlreadyDownloaded = false;
// First check mImageFilenameArray
String imageFilename = mImageFilenameArray.get(targetIndex);
if (imageFilename != null) {
File file = new File(mDir, imageFilename);
if (file.exists())
isAlreadyDownloaded = true;
} else {
// Can not find image file name, just guess
for (String possibleFilename : EhUtils.getPossibleImageFilenames(targetIndex)) {
File file = new File(mDir, possibleFilename);
if (file.exists()) {
isAlreadyDownloaded = true;
mImageFilenameArray.set(targetIndex, possibleFilename);
break;
}
}
}
if (isAlreadyDownloaded) {
Log.d(TAG, "isAlreadyDownloaded");
updateDownload(targetIndex);
continue;
}
mRequstingIndexSet.add(targetIndex);
listener.index = targetIndex;
String pageToken = null;
while (true) {
// Check stop
if (mStopWork || mPauseWork)
break;
pageToken = mPageTokeArray.get(targetIndex);
if (pageToken == null) {
addNoTokenIndex(targetIndex);
synchronized(mWorkerLock) {
try {mWorkerLock.wait();}
catch (InterruptedException e) {}
}
} else {
// Get valid page token, jump out of loop
break;
}
}
// Parser image page
for (int i = 0; i < 2 && !mStopWork && !mPauseWork; i++) {
hh.reset();
ipp.reset();
String body = hh.get(EhClient.getPageUrl(mGid, pageToken, targetIndex + 1, mMode)
+ ((i == 1) ? "?nl=48" : ""));
if (ipp.parser(body, mMode)) {
// Download image
HttpHelper.DownloadControlor c = mControlorArray[mIndex];
if (Config.getDownloadOriginImage() && ipp.originalImageUrl != null) {
Log.d(TAG, "Download origin image");
// Download orgin image
String imageUrl = ipp.originalImageUrl;
// Guess filename
String filename = EhUtils.getImageFilename(targetIndex, "jpg");
// Download
hh.reset();
if (hh.downloadOriginEhImage(imageUrl, mDir, filename, false, c, listener) == null) {
if (hh.getException() instanceof HttpHelper.BandwidthExceededException) {
// Get 509
show509Notification(mContext);
break;
} else {
String newImageUrl = hh.getLastUrl();
if (newImageUrl != null) {
// Try download from proxy
hh.reset();
if (hh.downloadEhImage(newImageUrl, mDir, filename, true, c, listener) == null) {
// Download failed
Log.d(TAG, "Download failed");
} else {
// Download successful
Log.d(TAG, "Download successful");
}
} else {
// Download failed
Log.d(TAG, "Download failed");
}
}
} else {
// Download successful
Log.d(TAG, "Download successful");
}
// No need to refresh image url
break;
} else {
// Download scale image
String imageUrl = ipp.imageUrl;
//Check 509 gif
boolean get509 = false;
int length = URL_ARRAY_509.length;
for (int j = 0; j < length; j++) {
if (URL_ARRAY_509[j].equals(imageUrl)) {
// Get 509 gif here
get509 = true;
break;
}
}
if (get509) {
show509Notification(mContext);
break;
}
// Guess filename
String filename = EhUtils.getImageFilename(targetIndex, Utils.getExtension(imageUrl, "jpg"));
// Don't put into mImageFilenameArray, because extension maybe be wrong
// mImageFilenameArray.set(targetIndex, filename);
hh.reset();
if (hh.downloadEhImage(imageUrl, mDir, filename, false, c, listener) == null) {
hh.reset();
if (hh.downloadEhImage(imageUrl, mDir, filename, true, c, listener) == null) {
if (i == 1) {
// TODO download error
Log.d(TAG, "download error");
}
} else {
// download ok
Log.d(TAG, "download ok proxy");
break;
}
} else {
// download ok
Log.d(TAG, "download ok");
break;
}
}
} else {
// TODO parser error, Do somthing
Log.d(TAG, "parser error");
break;
}
}
// Remove from requsting set
mRequstingIndexSet.remove(targetIndex);
}
// Worker over
synchronized(mWorkerArray) {
// Remove from mWorkerArray
mWorkerArray[mIndex] = null;
mControlorArray[mIndex] = null;
boolean allNull = true;
for (Worker w : mWorkerArray)
if (w != null) {allNull = false; break;}
synchronized (mDownloadLock) {
if (allNull && mDownloadMode && mLfd != null && mDownloadIndexSet != null) {
mLfd.onDownloadOver(mGid,
getMaxEnsureIndex() + 1 - mDownloadIndexSet.size());
}
}
}
Log.d(TAG, "ExDownloader Worker over");
}
}
public class HttpDownloadListener implements HttpHelper.OnDownloadListener {
public int index;
private float lastPercent;
private int lastDownloadSize;
@Override
public void onDownloadStartConnect() {
lastPercent = 0.0f;
lastDownloadSize = 0;
if (mLfis != null)
mLfis.onDownloadStart(index);
}
@Override
public void onDownloadStartDownload(int totalSize) {
lastPercent = 0.0f;
lastDownloadSize = 0;
}
@Override
public void onDownloadStatusUpdate(int downloadSize, int totalSize) {
if (totalSize == -1)
return;
float percent = (float)downloadSize / totalSize;
if (percent - lastPercent >= 0.05f) {
lastPercent = percent;
if (mLfis != null)
mLfis.onDownloading(index, percent);
}
// Update download size
int accumulateSize = downloadSize - lastDownloadSize;
lastDownloadSize = downloadSize;
mAccumulateSize += accumulateSize;
long curTime = System.currentTimeMillis();
long interval = curTime - mLastUpdateSpeedTime;
if (interval > UPDATE_SPEED_INTERVAL) {
if (mDownloadMode && mLfd != null)
mLfd.onUpdateSpeed(mGid, (int)(mAccumulateSize * 1000 / interval));
mLastUpdateSpeedTime = curTime;
mAccumulateSize = 0;
}
}
@Override
public void onDownloadOver(int status, String eMsg) {
if (status == HttpHelper.DOWNLOAD_OK_CODE) {
mRequstingIndexSet.remove(index);
updateDownload(index);
}
if (mLfis != null && status == HttpHelper.DOWNLOAD_OK_CODE)
mLfis.onDownloadComplete(index);
else if (mLfis != null && status == HttpHelper.DOWNLOAD_FAIL_CODE)
mLfis.onDownloadFail(index);
lastPercent = 0.0f;
lastDownloadSize = 0;
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import android.content.Context;
import android.util.SparseArray;
public class ExDownloaderManager {
private static final String TAG = ExDownloaderManager.class.getSimpleName();
private final Context mContext;
private final SparseArray<ExDownloader> mExDownloadList;
private static ExDownloaderManager sInstance;
public static void createInstance(Context context) {
sInstance = new ExDownloaderManager(context.getApplicationContext());
}
public static ExDownloaderManager getInstance() {
return sInstance;
}
private ExDownloaderManager(Context context) {
mContext = context;
mExDownloadList = new SparseArray<ExDownloader>();
}
public synchronized ExDownloader getExDownloader(int gid, String token, String title, int mode) {
ExDownloader exDownloader = mExDownloadList.get(gid);
if (exDownloader == null) {
exDownloader = new ExDownloader(mContext, gid, token, title, mode);
mExDownloadList.append(gid, exDownloader);
}
exDownloader.occupy();
return exDownloader;
}
public synchronized void freeExDownloader(ExDownloader exDownloader) {
exDownloader.free();
if (exDownloader.isOrphans()) {
exDownloader.stop();
mExDownloadList.remove(exDownloader.getGid());
}
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hippo.ehviewer.util.Utils;
public class ImagePageParser {
public String imageUrl;
public String originalImageUrl;
public int gid;
public String token;
public void reset() {
imageUrl = null;
originalImageUrl = null;
gid = 0;
token = null;
}
public boolean parser(String body, int mode) {
Pattern p;
Matcher m;
if (body == null)
return false;
if (mode ==EhClient.MODE_LOFI) {
p = Pattern.compile("<img id=\"sm\" src=\"([^\"]+)\"[^>]+>");
m = p.matcher(body);
if (m.find())
imageUrl = Utils.unescapeXml(m.group(1));
else
return false;
p = Pattern.compile("<a href=\"(?:http|https)://.+?/g/(\\d+)/(\\w+)/?\">Back</a>");
m = p.matcher(body);
if (m.find()) {
gid = Integer.parseInt(m.group(1));
token = m.group(2);
return true;
} else {
return false;
}
} else {
p = Pattern.compile("<div id=\"i3\">.+?<img id=\"img\" src=\"(.+?)\""
+ ".+?"
+ "<div id=\"i5\"><div class=\"sb\"><a href=\"(?:http|https)://.+?/g/(\\d+)/(\\w+)");
m = p.matcher(body);
if (m.find()) {
imageUrl = Utils.unescapeXml(m.group(1));
gid = Integer.parseInt(m.group(2));
token = m.group(3);
p = Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">");
m = p.matcher(body);
if (m.find()) {
originalImageUrl = Utils.unescapeXml(
new StringBuilder(m.group(1)).append("fullimg.php").append(m.group(2)).toString());
}
return true;
} else {
return false;
}
}
}
}
@@ -0,0 +1,230 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hippo.ehviewer.data.GalleryInfo;
import com.hippo.ehviewer.data.LofiGalleryInfo;
import com.hippo.ehviewer.util.EhUtils;
import com.hippo.ehviewer.util.Utils;
public class ListParser {
public static final int TARGET_PAGE_IS_LAST = -1;
public static final int ALL = 0x1;
public static final int NOT_FOUND = 0x2;
public static final int PARSER_ERROR = -1;
public static final int INDEX_ERROR = -2;
public int pageNum;
public ArrayList<GalleryInfo> giList;
/**
* If NOT_FOUND maxPage is 0,
* if can not get maxPage, set it Integer.MAX_VALUE
*
* @param pageContext
* @param mode
* @return
*/
public int parser(String body, int mode) {
Pattern p;
Matcher m;
if (body == null)
return 0;
giList = new ArrayList<GalleryInfo>(25);
switch (mode) {
case EhClient.MODE_LOFI:
p = Pattern.compile("<td class=\"ii\"><a href=\"(.+?)\">" // detail url
+ "<img src=\"(.+?)\".+?/>" // thumb url
+ ".+?<a class=\"b\" href=\".+?\">(.+?)</a>" // title
+ ".+?<td class=\"ik ip\">Posted:</td><td class=\"ip\">(.+?)</td>" // Posted and uploader
+ "</tr><tr><td class=\"ik\">Category:</td><td>(.+?)</td>" // Category
+ "</tr><tr><td class=\"ik\">Tags:</td><td>(.+?)</td>" // Tags
+ "</tr><tr><td class=\"ik\">Rating:</td><td class=\"ir\">(.+?)</td>"); // rating
m = p.matcher(body);
DetailUrlParser dup = new DetailUrlParser();
while (m.find()) {
LofiGalleryInfo lgi = new LofiGalleryInfo();
String[] pau = new String[2];
if (dup.parser(m.group(1))) {
lgi.gid = dup.gid;
lgi.token = dup.token;
} else {
continue;
}
lgi.thumb = Utils.unescapeXml(m.group(2));
lgi.title = Utils.unescapeXml(m.group(3));
if (getPostedAndUploader(m.group(4), pau)) {
lgi.posted = pau[0];
lgi.uploader = pau[1];
} else {
continue;
}
lgi.category = EhUtils.getCategory(m.group(5));
String tags = m.group(6);
if (tags.equals("-"))
lgi.lofiTags = null;
else
lgi.lofiTags = tags.split(", ");
String rating = m.group(7);
if (rating.equals("-"))
lgi.rating = Float.NaN;
else
lgi.rating = getStartNum(rating);
lgi.generateSLang();
giList.add(lgi);
}
if (giList.size() == 0) {
if (body.contains("No hits found</div>")) {
pageNum = 0;
return NOT_FOUND;
} else if (body.contains("No more hits found</div>")) {
return INDEX_ERROR;
} else {
return PARSER_ERROR;
}
} else {
if (!body.contains("Next Page &gt;</a>"))
pageNum = TARGET_PAGE_IS_LAST;
else
pageNum = Integer.MAX_VALUE;
return ALL;
}
case EhClient.MODE_G:
case EhClient.MODE_EX:
default:
p = Pattern.compile("<a[^<>]+>([\\d]+)</a></td><td[^<>]+>(?:<a[^<>]+>)?&");
m = p.matcher(body);
if (m.find()) {
pageNum = Integer.parseInt(m.group(1));
} else if (body.contains("No hits found</p>")) {
pageNum = 0;
return NOT_FOUND;
} else {
return PARSER_ERROR;
}
p = Pattern.compile("<td class=\"itdc\">(?:<a.+?>)?<img.+?alt=\"(.+?)\".+?/>(?:</a>)?</td>" // category
+ "<td.+?>(.+?)</td>" // posted
+ "<td.+?><div.+?><div.+?height:(\\d+)px; width:(\\d+)px\">"
+ "(?:<img.+?src=\"(.+?)\".+?alt=\"(.+?)\" style.+?/>"
+ "|init~([^<>\"~]+~[^<>\"~]+)~([^<>]+))" // thumb and title
+ "</div>"
+ ".+?"
+ "<div class=\"it5\"><a href=\"([^<>\"]+)\"[^<>]+>(.+?)</a></div>" // url and title
+ ".+?"
+ "<div class=\"ir it4r\" style=\"([^<>\"]+)\">" // rating
+ ".+?"
+ "<td class=\"itu\"><div><a.+?>(.+?)</a>"); // uploader
m = p.matcher(body);
while (m.find()) {
GalleryInfo gi = new GalleryInfo();
gi.category = EhUtils.getCategory(m.group(1));
gi.posted = m.group(2);
gi.thumbHeight = Integer.parseInt(m.group(3));
gi.thumbWidth = Integer.parseInt(m.group(4));
if (m.group(5) == null) {
gi.thumb = Utils.unescapeXml("http://"
+ m.group(7).replace('~', '/'));
gi.title = Utils.unescapeXml(m.group(8));
} else {
gi.thumb = Utils.unescapeXml(m.group(5));
gi.title = Utils.unescapeXml(m.group(6));
}
Pattern pattern = Pattern
.compile("/(\\d+)/(\\w+)");
Matcher matcher = pattern.matcher(m.group(9));
if (matcher.find()) {
gi.gid = Integer.parseInt(matcher.group(1));
gi.token = matcher.group(2);
} else
continue;
gi.rating = Float.parseFloat(getRate(m.group(11)));
gi.uploader = m.group(12);
gi.generateSLang();
giList.add(gi);
}
return ALL;
}
}
private String getRate(String rawRate) {
Pattern p = Pattern.compile("\\d+px");
Matcher m = p.matcher(rawRate);
int num1;
int num2;
int rate = 5;
String re;
if (m.find())
num1 = Integer.parseInt(m.group().replace("px", ""));
else
return null;
if (m.find())
num2 = Integer.parseInt(m.group().replace("px", ""));
else
return null;
rate = rate - num1 / 16;
if (num2 == 21) {
rate--;
re = Integer.toString(rate);
re = re + ".5";
} else
re = Integer.toString(rate);
return re;
}
private static final String PAU_SPACER = " by ";
private boolean getPostedAndUploader(String raw, String[] pau) {
int index = raw.indexOf(PAU_SPACER);
if (index == -1) {
return false;
} else {
pau[0] = raw.substring(0, index);
pau[1] = raw.substring(index + PAU_SPACER.length());
return true;
}
}
private int getStartNum(String str) {
int startNum = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '*')
startNum++;
}
return startNum;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hippo.ehviewer.data.LargePreviewList;
public class LofiDetailParser {
public boolean isLastPage;
public LargePreviewList preview;
public String emesg;
public boolean parser(String body) {
Pattern p;
Matcher m;
if (body == null)
return false;
// Get preview
p = Pattern.compile("<div class=\"gi\"[^>]+><a[^>]+href=\"([^>\"]+)\"[^>]+><img[^>]+src=\"([^>\"]+)\"[^>]+>");
m = p.matcher(body);
while (m.find()) {
if (preview == null)
preview = new LargePreviewList();
preview.addItem(m.group(2), m.group(1));
}
// Check is last page
p = Pattern.compile("<a[^>]+>Next Page &gt;</a>");
m = p.matcher(body);
if (m.find())
isLastPage = false;
else
isLastPage = true;
return preview != null;
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LoginParser {
public String displayname;
public String eMesg;
public boolean parser(String body) {
Pattern p;
Matcher m;
if (body == null)
return false;
p = Pattern.compile("<p>You are now logged in as: (.+?)<br />");
m = p.matcher(body);
if (m.find()) {
displayname = m.group(1);
return true;
} else {
p = Pattern.compile("(?:<h4>The error returned was:</h4>\\s*<p>(.+?)</p>)"
+ "|(?:<span class=\"postcolor\">(.+?)</span>)");
m = p.matcher(body);
if (m.find())
eMesg = m.group(1) == null ? m.group(2) : m.group(1);
else
eMesg = "Unknown error";
return false;
}
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import org.json.JSONException;
import org.json.JSONObject;
public class RateParser {
public float mRatingAvg;
public int mRatingCnt;
boolean parser(String body) {
if (body == null)
return false;
try {
JSONObject jsonObject = new JSONObject(body);
mRatingAvg = (float)jsonObject.getDouble("rating_avg");
mRatingCnt = jsonObject.getInt("rating_cnt");
return true;
} catch (JSONException e) {
return false;
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.ehclient;
import org.json.JSONException;
import org.json.JSONObject;
public class VoteParser {
public String mTagPane;
boolean parser(String body) {
if (body == null)
return false;
try {
JSONObject jsonObject = new JSONObject(body);
mTagPane = jsonObject.getString("tagpane");
return true;
} catch (JSONException e) {
return false;
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.exception;
public class FileException extends Exception {
public static final int MKDIR_ERROR = 0x0;
public static final int CREATE_FILE_ERROR = 0x1;
public static final int NOT_DIR = 0x2;
public static final int NOT_FILE = 0x3;
public static final int CANNOT_EXECUTE = 0x4;
public static final int CANNOT_READ = 0x5;
public static final int CANNOT_WRITE = 0x6;
private int mErrorCode;
private static final long serialVersionUID = 1L;
public FileException(int errorCode) {
mErrorCode = errorCode;
}
public int getErrorCode() {
return mErrorCode;
}
// TODO
public String getMessage() {
switch (mErrorCode) {
case MKDIR_ERROR:
return "mkdir error";
case CREATE_FILE_ERROR:
return "create file error";
case NOT_DIR:
return "it is not a dir";
case NOT_FILE:
return "it is not a file";
case CANNOT_EXECUTE:
return "can not execute";
case CANNOT_READ:
return "can not read";
case CANNOT_WRITE:
return "can not write";
default:
return "unknown error";
}
}
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.exception;
public class NoSdCardException extends Exception {
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.exception;
public class ParserException extends Exception{
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.exception;
public class StopRequestException extends Exception {
private static final long serialVersionUID = 1L;
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,92 @@
/*
* 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.hippo.ehviewer.gallery.anim;
import android.view.animation.Interpolator;
import com.hippo.ehviewer.util.MathUtils;
// Animation calculates a value according to the current input time.
//
// 1. First we need to use setDuration(int) to set the duration of the
// animation. The duration is in milliseconds.
// 2. Then we should call start(). The actual start time is the first value
// passed to calculate(long).
// 3. Each time we want to get an animation value, we call
// calculate(long currentTimeMillis) to ask the Animation to calculate it.
// The parameter passed to calculate(long) should be nonnegative.
// 4. Use get() to get that value.
//
// In step 3, onCalculate(float progress) is called so subclasses can calculate
// the value according to progress (progress is a value in [0,1]).
//
// Before onCalculate(float) is called, There is an optional interpolator which
// can change the progress value. The interpolator can be set by
// setInterpolator(Interpolator). If the interpolator is used, the value passed
// to onCalculate may be (for example, the overshoot effect).
//
// The isActive() method returns true after the animation start() is called and
// before calculate is passed a value which reaches the duration of the
// animation.
//
// The start() method can be called again to restart the Animation.
//
abstract public class Animation {
private static final long ANIMATION_START = -1;
private static final long NO_ANIMATION = -2;
private long mStartTime = NO_ANIMATION;
private int mDuration;
private Interpolator mInterpolator;
public void setInterpolator(Interpolator interpolator) {
mInterpolator = interpolator;
}
public void setDuration(int duration) {
mDuration = duration;
}
public void start() {
mStartTime = ANIMATION_START;
}
public void setStartTime(long time) {
mStartTime = time;
}
public boolean isActive() {
return mStartTime != NO_ANIMATION;
}
public void forceStop() {
mStartTime = NO_ANIMATION;
}
public boolean calculate(long currentTimeMillis) {
if (mStartTime == NO_ANIMATION) return false;
if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis;
int elapse = (int) (currentTimeMillis - mStartTime);
float x = MathUtils.clamp((float) elapse / mDuration, 0f, 1f);
Interpolator i = mInterpolator;
onCalculate(i != null ? i.getInterpolation(x) : x);
if (elapse >= mDuration) mStartTime = NO_ANIMATION;
return mStartTime != NO_ANIMATION;
}
abstract protected void onCalculate(float progress);
}
@@ -0,0 +1,25 @@
/*
* 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.hippo.ehviewer.gallery.anim;
import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
public abstract class CanvasAnimation extends Animation {
public abstract int getCanvasSaveFlags();
public abstract void apply(GLCanvas canvas);
}
@@ -0,0 +1,176 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.anim;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
import com.hippo.ehviewer.gallery.glrenderer.RawTexture;
import com.hippo.ehviewer.gallery.ui.GLView;
public class StateTransitionAnimation extends Animation {
public static class Spec {
public static final Spec OUTGOING;
public static final Spec INCOMING;
public static final Spec PHOTO_INCOMING;
private static final Interpolator DEFAULT_INTERPOLATOR =
new DecelerateInterpolator();
public int duration = 330;
public float backgroundAlphaFrom = 0;
public float backgroundAlphaTo = 0;
public float backgroundScaleFrom = 0;
public float backgroundScaleTo = 0;
public float contentAlphaFrom = 1;
public float contentAlphaTo = 1;
public float contentScaleFrom = 1;
public float contentScaleTo = 1;
public float overlayAlphaFrom = 0;
public float overlayAlphaTo = 0;
public float overlayScaleFrom = 0;
public float overlayScaleTo = 0;
public Interpolator interpolator = DEFAULT_INTERPOLATOR;
static {
OUTGOING = new Spec();
OUTGOING.backgroundAlphaFrom = 0.5f;
OUTGOING.backgroundAlphaTo = 0f;
OUTGOING.backgroundScaleFrom = 1f;
OUTGOING.backgroundScaleTo = 0f;
OUTGOING.contentAlphaFrom = 0.5f;
OUTGOING.contentAlphaTo = 1f;
OUTGOING.contentScaleFrom = 3f;
OUTGOING.contentScaleTo = 1f;
INCOMING = new Spec();
INCOMING.overlayAlphaFrom = 1f;
INCOMING.overlayAlphaTo = 0f;
INCOMING.overlayScaleFrom = 1f;
INCOMING.overlayScaleTo = 3f;
INCOMING.contentAlphaFrom = 0f;
INCOMING.contentAlphaTo = 1f;
INCOMING.contentScaleFrom = 0.25f;
INCOMING.contentScaleTo = 1f;
PHOTO_INCOMING = INCOMING;
}
private static Spec specForTransition(Transition t) {
switch (t) {
case Outgoing:
return Spec.OUTGOING;
case Incoming:
return Spec.INCOMING;
case PhotoIncoming:
return Spec.PHOTO_INCOMING;
case None:
default:
return null;
}
}
}
public static enum Transition { None, Outgoing, Incoming, PhotoIncoming }
private final Spec mTransitionSpec;
private float mCurrentContentScale;
private float mCurrentContentAlpha;
private float mCurrentBackgroundScale;
private float mCurrentBackgroundAlpha;
private float mCurrentOverlayScale;
private float mCurrentOverlayAlpha;
private RawTexture mOldScreenTexture;
public StateTransitionAnimation(Transition t, RawTexture oldScreen) {
this(Spec.specForTransition(t), oldScreen);
}
public StateTransitionAnimation(Spec spec, RawTexture oldScreen) {
mTransitionSpec = spec != null ? spec : Spec.OUTGOING;
setDuration(mTransitionSpec.duration);
setInterpolator(mTransitionSpec.interpolator);
mOldScreenTexture = oldScreen;
}
@Override
public boolean calculate(long currentTimeMillis) {
boolean retval = super.calculate(currentTimeMillis);
if (!isActive()) {
if (mOldScreenTexture != null) {
mOldScreenTexture.recycle();
mOldScreenTexture = null;
}
}
return retval;
}
@Override
protected void onCalculate(float progress) {
mCurrentContentScale = mTransitionSpec.contentScaleFrom
+ (mTransitionSpec.contentScaleTo - mTransitionSpec.contentScaleFrom) * progress;
mCurrentContentAlpha = mTransitionSpec.contentAlphaFrom
+ (mTransitionSpec.contentAlphaTo - mTransitionSpec.contentAlphaFrom) * progress;
mCurrentBackgroundAlpha = mTransitionSpec.backgroundAlphaFrom
+ (mTransitionSpec.backgroundAlphaTo - mTransitionSpec.backgroundAlphaFrom)
* progress;
mCurrentBackgroundScale = mTransitionSpec.backgroundScaleFrom
+ (mTransitionSpec.backgroundScaleTo - mTransitionSpec.backgroundScaleFrom)
* progress;
mCurrentOverlayScale = mTransitionSpec.overlayScaleFrom
+ (mTransitionSpec.overlayScaleTo - mTransitionSpec.overlayScaleFrom) * progress;
mCurrentOverlayAlpha = mTransitionSpec.overlayAlphaFrom
+ (mTransitionSpec.overlayAlphaTo - mTransitionSpec.overlayAlphaFrom) * progress;
}
private void applyOldTexture(GLView view, GLCanvas canvas, float alpha, float scale, boolean clear) {
if (mOldScreenTexture == null)
return;
if (clear) canvas.clearBuffer(view.getBackgroundColor());
canvas.save();
canvas.setAlpha(alpha);
int xOffset = view.getWidth() / 2;
int yOffset = view.getHeight() / 2;
canvas.translate(xOffset, yOffset);
canvas.scale(scale, scale, 1);
mOldScreenTexture.draw(canvas, -xOffset, -yOffset);
canvas.restore();
}
public void applyBackground(GLView view, GLCanvas canvas) {
if (mCurrentBackgroundAlpha > 0f) {
applyOldTexture(view, canvas, mCurrentBackgroundAlpha, mCurrentBackgroundScale, true);
}
}
public void applyContentTransform(GLView view, GLCanvas canvas) {
int xOffset = view.getWidth() / 2;
int yOffset = view.getHeight() / 2;
canvas.translate(xOffset, yOffset);
canvas.scale(mCurrentContentScale, mCurrentContentScale, 1);
canvas.translate(-xOffset, -yOffset);
canvas.setAlpha(mCurrentContentAlpha);
}
public void applyOverlay(GLView view, GLCanvas canvas) {
if (mCurrentOverlayAlpha > 0f) {
applyOldTexture(view, canvas, mCurrentOverlayAlpha, mCurrentOverlayScale, false);
}
}
}
@@ -0,0 +1,297 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.gallery.data;
import java.io.File;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import android.graphics.BitmapFactory;
import android.graphics.Movie;
import android.util.SparseArray;
import com.hippo.ehviewer.AppHandler;
import com.hippo.ehviewer.ehclient.ExDownloader;
import com.hippo.ehviewer.ehclient.ExDownloaderManager;
import com.hippo.ehviewer.gallery.image.Image;
import com.hippo.ehviewer.util.AutoExpandArray;
import com.hippo.ehviewer.util.BgThread;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.EhUtils;
import com.hippo.ehviewer.util.Log;
import com.hippo.ehviewer.util.Utils;
public class ImageSet implements ExDownloader.ListenerForImageSet {
private static final String TAG = ImageSet.class.getSimpleName();
/**
* These are code reture by getImage()
*/
public static final int RESULT_NONE = 0x0;
public static final int RESULT_DOWNLOADING = 0x1;
public static final int RESULT_DECODE = 0x2;
private final ExDownloaderManager mEdManager;
private final ExDownloader mExDownloader;
private final File mDir;
private final AutoExpandArray<String> mImageFilenameArray;
private ImageListener mListener;
private final Queue<DecodeInfo> mDecodeQueue = new ConcurrentLinkedQueue<DecodeInfo>();
private final SparseArray<Float> mPercentMap = new SparseArray<Float>(5);
private final DecodeWorker mDecodeWorker;
/** If true, then wake Worker, worker will stop **/
private volatile boolean mStopWork = false;
public class DecodeInfo {
public int index;
public String filename;
public DecodeInfo(int index, String filename) {
this.index = index;
this.filename = filename;
}
}
public interface ImageListener {
/**
* It will be invoke when ExDownloader download successful
*
* @param index
*/
void onGetImage(int index);
void onDownloading(int index, float percent);
/**
* It will be invoke when decode image over
*
* @param index
* @param res
*/
void onDecodeOver(int index, Object res);
}
public ImageSet(int gid, String token, String title, int startIndex) {
mDir = EhUtils.getGalleryDir(gid, title);
Utils.ensureDir(mDir, true);
// Init ExDownloader
mEdManager = ExDownloaderManager.getInstance();
mExDownloader = mEdManager.getExDownloader(gid, token, title, Config.getMode());
mExDownloader.setListenerForImageSet(this);
mExDownloader.setStartIndex(startIndex);
mImageFilenameArray = mExDownloader.getImageFilenameArray();
// Start decode worker
mDecodeWorker = new DecodeWorker();
mDecodeWorker.start();
}
public boolean isStop() {
return mStopWork;
}
public int getSize() {
return mExDownloader.getImageNum();
}
public int getEnsureSize() {
return mExDownloader.getMaxEnsureIndex() + 1;
}
public void setImageListener(ImageListener l) {
mListener = l;
}
public void addTargetIndex(int index) {
mExDownloader.addTargetIndex(index);
}
public void setStartIndex(int startIndex) {
mExDownloader.setStartIndex(startIndex);
}
public void setCurReadIndex(int index) {
mExDownloader.setCurReadIndex(index);
}
public String getTitle() {
return mExDownloader.getTitle();
}
public File getImageFile(int index) {
File file = null;
String name = getImageFilename(index);
if (name != null)
file = new File(mDir, name);
return file;
}
private String getImageFilename(int index) {
String filename = mImageFilenameArray.get(index);
if (filename == null) {
// Just guess filename
for (String possibleFilename : EhUtils.getPossibleImageFilenames(index)) {
if (new File(mDir, possibleFilename).exists()) {
// Get filename
filename = possibleFilename;
mImageFilenameArray.set(index, filename);
break;
}
}
}
return filename;
}
public void redownload(int index) {
String filename = getImageFilename(index);
if (filename != null) {
// Delete file
new File(mDir, filename).delete();
}
mExDownloader.addTargetIndex(index);
mListener.onGetImage(index);
}
public Object getImage(int index) {
String filename = getImageFilename(index);
Float percent;
if ((percent = mPercentMap.get(index)) != null) {
return percent;
} else if (mExDownloader.isDownloading(index)) {
// downloading
return RESULT_DOWNLOADING;
} else if (filename == null || !new File(mDir, filename).exists()) {
// Target index has not being downloading
return RESULT_NONE;
} else {
mDecodeQueue.offer(new DecodeInfo(index, filename));
// wake decode worker
synchronized(mDecodeQueue) {mDecodeQueue.notify();}
return RESULT_DECODE;
}
}
/**
* You must call it when you do not need it any more
*/
public void free() {
mStopWork = true;
// wake decode worker to wash wash sleep
synchronized(mDecodeQueue) {mDecodeQueue.notify();}
// Remove listener for ImageSet
mExDownloader.setListenerForImageSet(null);
// Free ExDownloader
mEdManager.freeExDownloader(mExDownloader);
}
private class DecodeWorker extends BgThread {
@Override
public void run() {
DecodeInfo decodeInfo;
while (true) {
// Check stop work or not
if (mStopWork)
break;
// Get decode task
synchronized (mDecodeQueue) {
if (mDecodeQueue.isEmpty()) {
try {
mDecodeQueue.wait();
} catch (InterruptedException e) {}
continue;
}
decodeInfo = mDecodeQueue.poll();
}
// do decode
final int index = decodeInfo.index;
String pathName = Utils.getPathName(mDir.getPath(), decodeInfo.filename);
Object res = null;
if (Utils.SUPPORT_IMAGE && Config.getCustomCodec()) {
res = Image.decodeFile(pathName, Config.getDecodeFormat());
} else {
if (Utils.getExtension(pathName, "jpg").equals("gif"))
res = Movie.decodeFile(pathName);
else
res = BitmapFactory.decodeFile(pathName);
}
final Object _res = res;
// Post to UI thread
AppHandler.getInstance().post(new Runnable() {
@Override
public void run() {
if (mListener != null)
mListener.onDecodeOver(index, _res);
}
});
}
Log.d(TAG, "DecodeWorker stop working");
}
}
@Override
public void onDownloadStart(final int index) {
AppHandler.getInstance().post(new Runnable() {
@Override
public void run() {
if (mListener != null)
mListener.onDownloading(index, 0.0f);
}
});
}
@Override
public void onDownloading(final int index, final float percent) {
mPercentMap.append(index, percent);
AppHandler.getInstance().post(new Runnable() {
@Override
public void run() {
if (mListener != null)
mListener.onDownloading(index, percent);
}
});
}
@Override
public void onDownloadComplete(final int index) {
mPercentMap.remove(index);
AppHandler.getInstance().post(new Runnable() {
@Override
public void run() {
if (mListener != null)
mListener.onGetImage(index);
}
});
}
@Override
public void onDownloadFail(int index) {
mPercentMap.remove(index);
}
}
@@ -0,0 +1,218 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import java.util.WeakHashMap;
import android.graphics.RectF;
import android.util.Log;
import com.hippo.ehviewer.util.MathUtils;
// BasicTexture is a Texture corresponds to a real GL texture.
// The state of a BasicTexture indicates whether its data is loaded to GL memory.
// If a BasicTexture is loaded into GL memory, it has a GL texture id.
public abstract class BasicTexture implements Texture {
private static final String TAG = "BasicTexture";
protected static final int UNSPECIFIED = -1;
protected static final int STATE_UNLOADED = 0;
protected static final int STATE_LOADED = 1;
protected static final int STATE_ERROR = -1;
// Log a warning if a texture is larger along a dimension
private static final int MAX_TEXTURE_SIZE = 4096;
protected int mId = -1;
protected int mState;
protected int mWidth = UNSPECIFIED;
protected int mHeight = UNSPECIFIED;
protected int mTextureWidth;
protected int mTextureHeight;
private boolean mHasBorder;
protected GLCanvas mCanvasRef = null;
private static WeakHashMap<BasicTexture, Object> sAllTextures
= new WeakHashMap<BasicTexture, Object>();
@SuppressWarnings("rawtypes")
private static ThreadLocal sInFinalizer = new ThreadLocal();
protected BasicTexture(GLCanvas canvas, int id, int state) {
setAssociatedCanvas(canvas);
mId = id;
mState = state;
synchronized (sAllTextures) {
sAllTextures.put(this, null);
}
}
protected BasicTexture() {
this(null, 0, STATE_UNLOADED);
}
protected void setAssociatedCanvas(GLCanvas canvas) {
mCanvasRef = canvas;
}
/**
* Sets the content size of this texture. In OpenGL, the actual texture
* size must be of power of 2, the size of the content may be smaller.
*/
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
mTextureWidth = width > 0 ? MathUtils.nextPowerOf2(width) : 0;
mTextureHeight = height > 0 ? MathUtils.nextPowerOf2(height) : 0;
if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {
Log.w(TAG, String.format("texture is too large: %d x %d",
mTextureWidth, mTextureHeight), new Exception());
}
}
public boolean isFlippedVertically() {
return false;
}
public int getId() {
return mId;
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
// Returns the width rounded to the next power of 2.
public int getTextureWidth() {
return mTextureWidth;
}
// Returns the height rounded to the next power of 2.
public int getTextureHeight() {
return mTextureHeight;
}
// Returns true if the texture has one pixel transparent border around the
// actual content. This is used to avoid jigged edges.
//
// The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap
// mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially
// covered by the texture will use the color of the edge texel. If we add
// the transparent border, the color of the edge texel will be mixed with
// appropriate amount of transparent.
//
// Currently our background is black, so we can draw the thumbnails without
// enabling blending.
public boolean hasBorder() {
return mHasBorder;
}
protected void setBorder(boolean hasBorder) {
mHasBorder = hasBorder;
}
@Override
public void draw(GLCanvas canvas, int x, int y) {
canvas.drawTexture(this, x, y, getWidth(), getHeight());
}
@Override
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
canvas.drawTexture(this, x, y, w, h);
}
public void draw(GLCanvas canvas, RectF source, RectF target) {
canvas.drawTexture(this, source, target);
}
// onBind is called before GLCanvas binds this texture.
// It should make sure the data is uploaded to GL memory.
abstract protected boolean onBind(GLCanvas canvas);
// Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
abstract protected int getTarget();
public boolean isLoaded() {
return mState == STATE_LOADED;
}
// recycle() is called when the texture will never be used again,
// so it can free all resources.
public void recycle() {
freeResource();
}
// yield() is called when the texture will not be used temporarily,
// so it can free some resources.
// The default implementation unloads the texture from GL memory, so
// the subclass should make sure it can reload the texture to GL memory
// later, or it will have to override this method.
public void yield() {
freeResource();
}
private void freeResource() {
GLCanvas canvas = mCanvasRef;
if (canvas != null && mId != -1) {
canvas.unloadTexture(this);
mId = -1; // Don't free it again.
}
mState = STATE_UNLOADED;
setAssociatedCanvas(null);
}
@Override
@SuppressWarnings("unchecked")
protected void finalize() {
sInFinalizer.set(BasicTexture.class);
recycle();
sInFinalizer.set(null);
}
// This is for deciding if we can call Bitmap's recycle().
// We cannot call Bitmap's recycle() in finalizer because at that point
// the finalizer of Bitmap may already be called so recycle() will crash.
public static boolean inFinalizer() {
return sInFinalizer.get() != null;
}
public static void yieldAllTextures() {
synchronized (sAllTextures) {
for (BasicTexture t : sAllTextures.keySet()) {
t.yield();
}
}
}
public static void invalidateAllTextures() {
synchronized (sAllTextures) {
for (BasicTexture t : sAllTextures.keySet()) {
t.mState = STATE_UNLOADED;
t.setAssociatedCanvas(null);
}
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import junit.framework.Assert;
import android.graphics.Bitmap;
// BitmapTexture is a texture whose content is specified by a fixed Bitmap.
//
// The texture does not own the Bitmap. The user should make sure the Bitmap
// is valid during the texture's lifetime. When the texture is recycled, it
// does not free the Bitmap.
public class BitmapTexture extends UploadedTexture {
protected Bitmap mContentBitmap;
public BitmapTexture(Bitmap bitmap) {
this(bitmap, false);
}
public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
super(hasBorder);
Assert.assertTrue(bitmap != null && !bitmap.isRecycled());
mContentBitmap = bitmap;
}
@Override
protected void onFreeBitmap(Bitmap bitmap) {
// Do nothing.
}
@Override
protected Bitmap onGetBitmap() {
return mContentBitmap;
}
public Bitmap getBitmap() {
return mContentBitmap;
}
}
@@ -0,0 +1,54 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
// CanvasTexture is a texture whose content is the drawing on a Canvas.
// The subclasses should override onDraw() to draw on the bitmap.
// By default CanvasTexture is not opaque.
abstract class CanvasTexture extends UploadedTexture {
protected Canvas mCanvas;
private final Config mConfig;
public CanvasTexture(int width, int height) {
mConfig = Config.ARGB_8888;
setSize(width, height);
setOpaque(false);
}
@Override
@SuppressLint("WrongCall")
protected Bitmap onGetBitmap() {
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig);
mCanvas = new Canvas(bitmap);
onDraw(mCanvas, bitmap);
return bitmap;
}
@Override
protected void onFreeBitmap(Bitmap bitmap) {
if (!inFinalizer()) {
bitmap.recycle();
}
}
abstract protected void onDraw(Canvas canvas, Bitmap backing);
}
@@ -0,0 +1,63 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import com.hippo.ehviewer.util.Utils;
// ColorTexture is a texture which fills the rectangle with the specified color.
public class ColorTexture implements Texture {
private final int mColor;
private int mWidth;
private int mHeight;
public ColorTexture(int color) {
mColor = color;
mWidth = 1;
mHeight = 1;
}
@Override
public void draw(GLCanvas canvas, int x, int y) {
draw(canvas, x, y, mWidth, mHeight);
}
@Override
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
canvas.fillRect(x, y, w, h, mColor);
}
@Override
public boolean isOpaque() {
return Utils.isOpaque(mColor);
}
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
// ExtTexture is a texture whose content comes from a external texture.
// Before drawing, setSize() should be called.
public class ExtTexture extends BasicTexture {
private final int mTarget;
public ExtTexture(GLCanvas canvas, int target) {
GLId glId = canvas.getGLId();
mId = glId.generateTexture();
mTarget = target;
}
private void uploadToCanvas(GLCanvas canvas) {
canvas.setTextureParameters(this);
setAssociatedCanvas(canvas);
mState = STATE_LOADED;
}
@Override
protected boolean onBind(GLCanvas canvas) {
if (!isLoaded()) {
uploadToCanvas(canvas);
}
return true;
}
@Override
public int getTarget() {
return mTarget;
}
@Override
public boolean isOpaque() {
return true;
}
@Override
public void yield() {
// we cannot free the texture because we have no backup.
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
// FadeInTexture is a texture which begins with a color, then gradually animates
// into a given texture.
public class FadeInTexture extends FadeTexture implements Texture {
@SuppressWarnings("unused")
private static final String TAG = "FadeInTexture";
private final int mColor;
private final TiledTexture mTexture;
public FadeInTexture(int color, TiledTexture texture) {
super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
mColor = color;
mTexture = texture;
}
@Override
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
if (isAnimating()) {
mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h);
} else {
mTexture.draw(canvas, x, y, w, h);
}
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
// FadeOutTexture is a texture which begins with a given texture, then gradually animates
// into fading out totally.
public class FadeOutTexture extends FadeTexture {
@SuppressWarnings("unused")
private static final String TAG = "FadeOutTexture";
private final BasicTexture mTexture;
public FadeOutTexture(BasicTexture texture) {
super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
mTexture = texture;
}
@Override
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
if (isAnimating()) {
canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
canvas.setAlpha(getRatio());
mTexture.draw(canvas, x, y, w, h);
canvas.restore();
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
import com.hippo.ehviewer.gallery.ui.AnimationTime;
import com.hippo.ehviewer.util.MathUtils;
// FadeTexture is a texture which fades the given texture along the time.
public abstract class FadeTexture implements Texture {
@SuppressWarnings("unused")
private static final String TAG = "FadeTexture";
// The duration of the fading animation in milliseconds
public static final int DURATION = 180;
private final long mStartTime;
private final int mWidth;
private final int mHeight;
private final boolean mIsOpaque;
private boolean mIsAnimating;
public FadeTexture(int width, int height, boolean opaque) {
mWidth = width;
mHeight = height;
mIsOpaque = opaque;
mStartTime = now();
mIsAnimating = true;
}
@Override
public void draw(GLCanvas canvas, int x, int y) {
draw(canvas, x, y, mWidth, mHeight);
}
@Override
public boolean isOpaque() {
return mIsOpaque;
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
public boolean isAnimating() {
if (mIsAnimating) {
if (now() - mStartTime >= DURATION) {
mIsAnimating = false;
}
}
return mIsAnimating;
}
protected float getRatio() {
float r = (float)(now() - mStartTime) / DURATION;
return MathUtils.clamp(1.0f - r, 0.0f, 1.0f);
}
private long now() {
return AnimationTime.get();
}
}
@@ -0,0 +1,215 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
//
// GLCanvas gives a convenient interface to draw using OpenGL.
//
// When a rectangle is specified in this interface, it means the region
// [x, x+width) * [y, y+height)
//
public interface GLCanvas {
public GLId getGLId();
// Tells GLCanvas the size of the underlying GL surface. This should be
// called before first drawing and when the size of GL surface is changed.
// This is called by GLRoot and should not be called by the clients
// who only want to draw on the GLCanvas. Both width and height must be
// nonnegative.
public abstract void setSize(int width, int height);
// Clear the drawing buffers. This should only be used by GLRoot.
public abstract void clearBuffer();
public abstract void clearBuffer(float[] argb);
// Sets and gets the current alpha, alpha must be in [0, 1].
public abstract void setAlpha(float alpha);
public abstract float getAlpha();
// (current alpha) = (current alpha) * alpha
public abstract void multiplyAlpha(float alpha);
// Change the current transform matrix.
public abstract void translate(float x, float y, float z);
public abstract void translate(float x, float y);
public abstract void scale(float sx, float sy, float sz);
public abstract void rotate(float angle, float x, float y, float z);
public abstract void multiplyMatrix(float[] mMatrix, int offset);
// Pushes the configuration state (matrix, and alpha) onto
// a private stack.
public abstract void save();
// Same as save(), but only save those specified in saveFlags.
public abstract void save(int saveFlags);
public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
public static final int SAVE_FLAG_ALPHA = 0x01;
public static final int SAVE_FLAG_MATRIX = 0x02;
// Pops from the top of the stack as current configuration state (matrix,
// alpha, and clip). This call balances a previous call to save(), and is
// used to remove all modifications to the configuration state since the
// last save call.
public abstract void restore();
// Draws a line using the specified paint from (x1, y1) to (x2, y2).
// (Both end points are included).
public abstract void drawLine(float x1, float y1, float x2, float y2, GLPaint paint);
// Draws a rectangle using the specified paint from (x1, y1) to (x2, y2).
// (Both end points are included).
public abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);
// Fills the specified rectangle with the specified color.
public abstract void fillRect(float x, float y, float width, float height, int color);
// Draws a texture to the specified rectangle.
public abstract void drawTexture(
BasicTexture texture, int x, int y, int width, int height);
public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
int uvBuffer, int indexBuffer, int indexCount);
// Draws the source rectangle part of the texture to the target rectangle.
public abstract void drawTexture(BasicTexture texture, RectF source, RectF target);
// Draw a texture with a specified texture transform.
public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform,
int x, int y, int w, int h);
// Draw two textures to the specified rectangle. The actual texture used is
// from * (1 - ratio) + to * ratio
// The two textures must have the same size.
public abstract void drawMixed(BasicTexture from, int toColor,
float ratio, int x, int y, int w, int h);
// Draw a region of a texture and a specified color to the specified
// rectangle. The actual color used is from * (1 - ratio) + to * ratio.
// The region of the texture is defined by parameter "src". The target
// rectangle is specified by parameter "target".
public abstract void drawMixed(BasicTexture from, int toColor,
float ratio, RectF src, RectF target);
// Unloads the specified texture from the canvas. The resource allocated
// to draw the texture will be released. The specified texture will return
// to the unloaded state. This function should be called only from
// BasicTexture or its descendant
public abstract boolean unloadTexture(BasicTexture texture);
// Delete the specified buffer object, similar to unloadTexture.
public abstract void deleteBuffer(int bufferId);
// Delete the textures and buffers in GL side. This function should only be
// called in the GL thread.
public abstract void deleteRecycledResources();
// Dump statistics information and clear the counters. For debug only.
public abstract void dumpStatisticsAndClear();
public abstract void beginRenderTarget(RawTexture texture);
public abstract void endRenderTarget();
/**
* Sets texture parameters to use GL_CLAMP_TO_EDGE for both
* GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be
* GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER.
* bindTexture() must be called prior to this.
*
* @param texture The texture to set parameters on.
*/
public abstract void setTextureParameters(BasicTexture texture);
/**
* Initializes the texture to a size by calling texImage2D on it.
*
* @param texture The texture to initialize the size.
* @param format The texture format (e.g. GL_RGBA)
* @param type The texture type (e.g. GL_UNSIGNED_BYTE)
*/
public abstract void initializeTextureSize(BasicTexture texture, int format, int type);
/**
* Initializes the texture to a size by calling texImage2D on it.
*
* @param texture The texture to initialize the size.
* @param bitmap The bitmap to initialize the bitmap with.
*/
public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap);
/**
* Calls glTexSubImage2D to upload a bitmap to the texture.
*
* @param texture The target texture to write to.
* @param xOffset Specifies a texel offset in the x direction within the
* texture array.
* @param yOffset Specifies a texel offset in the y direction within the
* texture array.
* @param format The texture format (e.g. GL_RGBA)
* @param type The texture type (e.g. GL_UNSIGNED_BYTE)
*/
public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset,
Bitmap bitmap,
int format, int type);
/**
* Generates buffers and uploads the buffer data.
*
* @param buffer The buffer to upload
* @return The buffer ID that was generated.
*/
public abstract int uploadBuffer(java.nio.FloatBuffer buffer);
/**
* Generates buffers and uploads the element array buffer data.
*
* @param buffer The buffer to upload
* @return The buffer ID that was generated.
*/
public abstract int uploadBuffer(java.nio.ByteBuffer buffer);
/**
* After LightCycle makes GL calls, this method is called to restore the GL
* configuration to the one expected by GLCanvas.
*/
public abstract void recoverFromLightCycle();
/**
* Gets the bounds given by x, y, width, and height as well as the internal
* matrix state. There is no special handling for non-90-degree rotations.
* It only considers the lower-left and upper-right corners as the bounds.
*
* @param bounds The output bounds to write to.
* @param x The left side of the input rectangle.
* @param y The bottom of the input rectangle.
* @param width The width of the input rectangle.
* @param height The height of the input rectangle.
*/
public abstract void getBounds(Rect bounds, int x, int y, int width, int height);
}
@@ -0,0 +1,995 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11Ext;
import javax.microedition.khronos.opengles.GL11ExtensionPack;
import junit.framework.Assert;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.opengl.GLU;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.util.Log;
import com.hippo.ehviewer.util.IntArray;
import com.hippo.ehviewer.util.Utils;
public class GLES11Canvas implements GLCanvas {
private static final String TAG = "GLCanvasImp";
private static final float OPAQUE_ALPHA = 0.95f;
private static final int OFFSET_FILL_RECT = 0;
private static final int OFFSET_DRAW_LINE = 4;
private static final int OFFSET_DRAW_RECT = 6;
private static final float[] BOX_COORDINATES = {
0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle
0, 0, 1, 1, // used for drawing a line
0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle
private final GL11 mGL;
private final float mMatrixValues[] = new float[16];
private final float mTextureMatrixValues[] = new float[16];
// The results of mapPoints are stored in this buffer, and the order is
// x1, y1, x2, y2.
private final float mMapPointsBuffer[] = new float[4];
private final float mTextureColor[] = new float[4];
private final int mBoxCoords;
private final GLState mGLState;
private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>();
private float mAlpha;
private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
private ConfigState mRecycledRestoreAction;
private final RectF mDrawTextureSourceRect = new RectF();
private final RectF mDrawTextureTargetRect = new RectF();
private final float[] mTempMatrix = new float[32];
private final IntArray mUnboundTextures = new IntArray();
private final IntArray mDeleteBuffers = new IntArray();
private int mScreenWidth;
private int mScreenHeight;
private final boolean mBlendEnabled = true;
private final int mFrameBuffer[] = new int[1];
private static float[] sCropRect = new float[4];
private RawTexture mTargetTexture;
// Drawing statistics
int mCountDrawLine;
int mCountFillRect;
int mCountDrawMesh;
int mCountTextureRect;
int mCountTextureOES;
private static GLId mGLId = new GLES11IdImpl();
public GLES11Canvas(GL11 gl) {
mGL = gl;
mGLState = new GLState(gl);
// First create an nio buffer, then create a VBO from it.
int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE;
FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);
int[] name = new int[1];
mGLId.glGenBuffers(1, name, 0);
mBoxCoords = name[0];
gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE),
xyBuffer, GL11.GL_STATIC_DRAW);
gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
// Enable the texture coordinate array for Texture 1
gl.glClientActiveTexture(GL11.GL_TEXTURE1);
gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
gl.glClientActiveTexture(GL11.GL_TEXTURE0);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
// mMatrixValues and mAlpha will be initialized in setSize()
}
@Override
public void setSize(int width, int height) {
Assert.assertTrue(width >= 0 && height >= 0);
if (mTargetTexture == null) {
mScreenWidth = width;
mScreenHeight = height;
}
mAlpha = 1.0f;
GL11 gl = mGL;
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL11.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluOrtho2D(gl, 0, width, 0, height);
gl.glMatrixMode(GL11.GL_MODELVIEW);
gl.glLoadIdentity();
float matrix[] = mMatrixValues;
Matrix.setIdentityM(matrix, 0);
// to match the graphic coordinate system in android, we flip it vertically.
if (mTargetTexture == null) {
Matrix.translateM(matrix, 0, 0, height, 0);
Matrix.scaleM(matrix, 0, 1, -1, 1);
}
}
@Override
public void setAlpha(float alpha) {
Assert.assertTrue(alpha >= 0 && alpha <= 1);
mAlpha = alpha;
}
@Override
public float getAlpha() {
return mAlpha;
}
@Override
public void multiplyAlpha(float alpha) {
Assert.assertTrue(alpha >= 0 && alpha <= 1);
mAlpha *= alpha;
}
private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
}
@Override
public void drawRect(float x, float y, float width, float height, GLPaint paint) {
GL11 gl = mGL;
mGLState.setColorMode(paint.getColor(), mAlpha);
mGLState.setLineWidth(paint.getLineWidth());
saveTransform();
translate(x, y);
scale(width, height, 1);
gl.glLoadMatrixf(mMatrixValues, 0);
gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);
restoreTransform();
mCountDrawLine++;
}
@Override
public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
GL11 gl = mGL;
mGLState.setColorMode(paint.getColor(), mAlpha);
mGLState.setLineWidth(paint.getLineWidth());
saveTransform();
translate(x1, y1);
scale(x2 - x1, y2 - y1, 1);
gl.glLoadMatrixf(mMatrixValues, 0);
gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2);
restoreTransform();
mCountDrawLine++;
}
@Override
public void fillRect(float x, float y, float width, float height, int color) {
mGLState.setColorMode(color, mAlpha);
GL11 gl = mGL;
saveTransform();
translate(x, y);
scale(width, height, 1);
gl.glLoadMatrixf(mMatrixValues, 0);
gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
restoreTransform();
mCountFillRect++;
}
@Override
public void translate(float x, float y, float z) {
Matrix.translateM(mMatrixValues, 0, x, y, z);
}
// This is a faster version of translate(x, y, z) because
// (1) we knows z = 0, (2) we inline the Matrix.translateM call,
// (3) we unroll the loop
@Override
public void translate(float x, float y) {
float[] m = mMatrixValues;
m[12] += m[0] * x + m[4] * y;
m[13] += m[1] * x + m[5] * y;
m[14] += m[2] * x + m[6] * y;
m[15] += m[3] * x + m[7] * y;
}
@Override
public void scale(float sx, float sy, float sz) {
Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
}
@Override
public void rotate(float angle, float x, float y, float z) {
if (angle == 0) return;
float[] temp = mTempMatrix;
Matrix.setRotateM(temp, 0, angle, x, y, z);
Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);
System.arraycopy(temp, 16, mMatrixValues, 0, 16);
}
@Override
public void multiplyMatrix(float matrix[], int offset) {
float[] temp = mTempMatrix;
Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
System.arraycopy(temp, 0, mMatrixValues, 0, 16);
}
private void textureRect(float x, float y, float width, float height) {
GL11 gl = mGL;
saveTransform();
translate(x, y);
scale(width, height, 1);
gl.glLoadMatrixf(mMatrixValues, 0);
gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
restoreTransform();
mCountTextureRect++;
}
@Override
public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
int uvBuffer, int indexBuffer, int indexCount) {
float alpha = mAlpha;
if (!bindTexture(tex)) return;
mGLState.setBlendEnabled(mBlendEnabled
&& (!tex.isOpaque() || alpha < OPAQUE_ALPHA));
mGLState.setTextureAlpha(alpha);
// Reset the texture matrix. We will set our own texture coordinates
// below.
setTextureCoords(0, 0, 1, 1);
saveTransform();
translate(x, y);
mGL.glLoadMatrixf(mMatrixValues, 0);
mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer);
mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer);
mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP,
indexCount, GL11.GL_UNSIGNED_BYTE, 0);
mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
restoreTransform();
mCountDrawMesh++;
}
// Transforms two points by the given matrix m. The result
// {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.
private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) {
float[] r = mMapPointsBuffer;
// Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.
float x3 = m[0] * x1 + m[4] * y1 + m[12];
float y3 = m[1] * x1 + m[5] * y1 + m[13];
float w3 = m[3] * x1 + m[7] * y1 + m[15];
r[0] = x3 / w3;
r[1] = y3 / w3;
// Same for x2 y2.
float x4 = m[0] * x2 + m[4] * y2 + m[12];
float y4 = m[1] * x2 + m[5] * y2 + m[13];
float w4 = m[3] * x2 + m[7] * y2 + m[15];
r[2] = x4 / w4;
r[3] = y4 / w4;
return r;
}
private void drawBoundTexture(
BasicTexture texture, int x, int y, int width, int height) {
// Test whether it has been rotated or flipped, if so, glDrawTexiOES
// won't work
if (isMatrixRotatedOrFlipped(mMatrixValues)) {
if (texture.hasBorder()) {
setTextureCoords(
1.0f / texture.getTextureWidth(),
1.0f / texture.getTextureHeight(),
(texture.getWidth() - 1.0f) / texture.getTextureWidth(),
(texture.getHeight() - 1.0f) / texture.getTextureHeight());
} else {
setTextureCoords(0, 0,
(float) texture.getWidth() / texture.getTextureWidth(),
(float) texture.getHeight() / texture.getTextureHeight());
}
textureRect(x, y, width, height);
} else {
// draw the rect from bottom-left to top-right
float points[] = mapPoints(
mMatrixValues, x, y + height, x + width, y);
x = (int) (points[0] + 0.5f);
y = (int) (points[1] + 0.5f);
width = (int) (points[2] + 0.5f) - x;
height = (int) (points[3] + 0.5f) - y;
if (width > 0 && height > 0) {
((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);
mCountTextureOES++;
}
}
}
@Override
public void drawTexture(
BasicTexture texture, int x, int y, int width, int height) {
drawTexture(texture, x, y, width, height, mAlpha);
}
private void drawTexture(BasicTexture texture,
int x, int y, int width, int height, float alpha) {
if (width <= 0 || height <= 0) return;
mGLState.setBlendEnabled(mBlendEnabled
&& (!texture.isOpaque() || alpha < OPAQUE_ALPHA));
if (!bindTexture(texture)) return;
mGLState.setTextureAlpha(alpha);
drawBoundTexture(texture, x, y, width, height);
}
@Override
public void drawTexture(BasicTexture texture, RectF source, RectF target) {
if (target.width() <= 0 || target.height() <= 0) return;
// Copy the input to avoid changing it.
mDrawTextureSourceRect.set(source);
mDrawTextureTargetRect.set(target);
source = mDrawTextureSourceRect;
target = mDrawTextureTargetRect;
mGLState.setBlendEnabled(mBlendEnabled
&& (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
if (!bindTexture(texture)) return;
convertCoordinate(source, target, texture);
setTextureCoords(source);
mGLState.setTextureAlpha(mAlpha);
textureRect(target.left, target.top, target.width(), target.height());
}
@Override
public void drawTexture(BasicTexture texture, float[] mTextureTransform,
int x, int y, int w, int h) {
mGLState.setBlendEnabled(mBlendEnabled
&& (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
if (!bindTexture(texture)) return;
setTextureCoords(mTextureTransform);
mGLState.setTextureAlpha(mAlpha);
textureRect(x, y, w, h);
}
// This function changes the source coordinate to the texture coordinates.
// It also clips the source and target coordinates if it is beyond the
// bound of the texture.
private static void convertCoordinate(RectF source, RectF target,
BasicTexture texture) {
int width = texture.getWidth();
int height = texture.getHeight();
int texWidth = texture.getTextureWidth();
int texHeight = texture.getTextureHeight();
// Convert to texture coordinates
source.left /= texWidth;
source.right /= texWidth;
source.top /= texHeight;
source.bottom /= texHeight;
// Clip if the rendering range is beyond the bound of the texture.
float xBound = (float) width / texWidth;
if (source.right > xBound) {
target.right = target.left + target.width() *
(xBound - source.left) / source.width();
source.right = xBound;
}
float yBound = (float) height / texHeight;
if (source.bottom > yBound) {
target.bottom = target.top + target.height() *
(yBound - source.top) / source.height();
source.bottom = yBound;
}
}
@Override
public void drawMixed(BasicTexture from,
int toColor, float ratio, int x, int y, int w, int h) {
drawMixed(from, toColor, ratio, x, y, w, h, mAlpha);
}
private boolean bindTexture(BasicTexture texture) {
if (!texture.onBind(this)) return false;
int target = texture.getTarget();
mGLState.setTextureTarget(target);
mGL.glBindTexture(target, texture.getId());
return true;
}
private void setTextureColor(float r, float g, float b, float alpha) {
float[] color = mTextureColor;
color[0] = r;
color[1] = g;
color[2] = b;
color[3] = alpha;
}
private void setMixedColor(int toColor, float ratio, float alpha) {
//
// The formula we want:
// alpha * ((1 - ratio) * from + ratio * to)
//
// The formula that GL supports is in the form of:
// combo * from + (1 - combo) * to * scale
//
// So, we have combo = alpha * (1 - ratio)
// and scale = alpha * ratio / (1 - combo)
//
float combo = alpha * (1 - ratio);
float scale = alpha * ratio / (1 - combo);
// Specify the interpolation factor via the alpha component of
// GL_TEXTURE_ENV_COLORs.
// RGB component are get from toColor and will used as SRC1
float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
((toColor >>> 8) & 0xff) * colorScale,
(toColor & 0xff) * colorScale, combo);
GL11 gl = mGL;
gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
// Wire up the interpolation factor for RGB.
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
// Wire up the interpolation factor for alpha.
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
}
@Override
public void drawMixed(BasicTexture from, int toColor, float ratio,
RectF source, RectF target) {
if (target.width() <= 0 || target.height() <= 0) return;
if (ratio <= 0.01f) {
drawTexture(from, source, target);
return;
} else if (ratio >= 1) {
fillRect(target.left, target.top, target.width(), target.height(), toColor);
return;
}
float alpha = mAlpha;
// Copy the input to avoid changing it.
mDrawTextureSourceRect.set(source);
mDrawTextureTargetRect.set(target);
source = mDrawTextureSourceRect;
target = mDrawTextureTargetRect;
mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
|| !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
if (!bindTexture(from)) return;
// Interpolate the RGB and alpha values between both textures.
mGLState.setTexEnvMode(GL11.GL_COMBINE);
setMixedColor(toColor, ratio, alpha);
convertCoordinate(source, target, from);
setTextureCoords(source);
textureRect(target.left, target.top, target.width(), target.height());
mGLState.setTexEnvMode(GL11.GL_REPLACE);
}
private void drawMixed(BasicTexture from, int toColor,
float ratio, int x, int y, int width, int height, float alpha) {
// change from 0 to 0.01f to prevent getting divided by zero below
if (ratio <= 0.01f) {
drawTexture(from, x, y, width, height, alpha);
return;
} else if (ratio >= 1) {
fillRect(x, y, width, height, toColor);
return;
}
mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
|| !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
if (!bindTexture(from)) return;
// Interpolate the RGB and alpha values between both textures.
mGLState.setTexEnvMode(GL11.GL_COMBINE);
setMixedColor(toColor, ratio, alpha);
drawBoundTexture(from, x, y, width, height);
mGLState.setTexEnvMode(GL11.GL_REPLACE);
}
// TODO: the code only work for 2D should get fixed for 3D or removed
private static final int MSKEW_X = 4;
private static final int MSKEW_Y = 1;
private static final int MSCALE_X = 0;
private static final int MSCALE_Y = 5;
private static boolean isMatrixRotatedOrFlipped(float matrix[]) {
final float eps = 1e-5f;
return Math.abs(matrix[MSKEW_X]) > eps
|| Math.abs(matrix[MSKEW_Y]) > eps
|| matrix[MSCALE_X] < -eps
|| matrix[MSCALE_Y] > eps;
}
private static class GLState {
private final GL11 mGL;
private int mTexEnvMode = GL11.GL_REPLACE;
private float mTextureAlpha = 1.0f;
private int mTextureTarget = GL11.GL_TEXTURE_2D;
private boolean mBlendEnabled = true;
private float mLineWidth = 1.0f;
public GLState(GL11 gl) {
mGL = gl;
// Disable unused state
gl.glDisable(GL11.GL_LIGHTING);
// Enable used features
gl.glEnable(GL11.GL_DITHER);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glEnable(GL11.GL_TEXTURE_2D);
gl.glTexEnvf(GL11.GL_TEXTURE_ENV,
GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
// Set the background color
gl.glClearColor(0f, 0f, 0f, 0f);
gl.glEnable(GL11.GL_BLEND);
gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
// We use 565 or 8888 format, so set the alignment to 2 bytes/pixel.
gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2);
}
public void setTexEnvMode(int mode) {
if (mTexEnvMode == mode) return;
mTexEnvMode = mode;
mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode);
}
public void setLineWidth(float width) {
if (mLineWidth == width) return;
mLineWidth = width;
mGL.glLineWidth(width);
}
public void setTextureAlpha(float alpha) {
if (mTextureAlpha == alpha) return;
mTextureAlpha = alpha;
if (alpha >= OPAQUE_ALPHA) {
// The alpha is need for those texture without alpha channel
mGL.glColor4f(1, 1, 1, 1);
setTexEnvMode(GL11.GL_REPLACE);
} else {
mGL.glColor4f(alpha, alpha, alpha, alpha);
setTexEnvMode(GL11.GL_MODULATE);
}
}
public void setColorMode(int color, float alpha) {
setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA);
// Set mTextureAlpha to an invalid value, so that it will reset
// again in setTextureAlpha(float) later.
mTextureAlpha = -1.0f;
setTextureTarget(0);
float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
mGL.glColor4x(
Math.round(((color >> 16) & 0xFF) * prealpha),
Math.round(((color >> 8) & 0xFF) * prealpha),
Math.round((color & 0xFF) * prealpha),
Math.round(255 * prealpha));
}
// target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled.
public void setTextureTarget(int target) {
if (mTextureTarget == target) return;
if (mTextureTarget != 0) {
mGL.glDisable(mTextureTarget);
}
mTextureTarget = target;
if (mTextureTarget != 0) {
mGL.glEnable(mTextureTarget);
}
}
public void setBlendEnabled(boolean enabled) {
if (mBlendEnabled == enabled) return;
mBlendEnabled = enabled;
if (enabled) {
mGL.glEnable(GL11.GL_BLEND);
} else {
mGL.glDisable(GL11.GL_BLEND);
}
}
}
@Override
public void clearBuffer(float[] argb) {
if(argb != null && argb.length == 4) {
mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]);
} else {
mGL.glClearColor(0, 0, 0, 1);
}
mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
}
@Override
public void clearBuffer() {
clearBuffer(null);
}
private void setTextureCoords(RectF source) {
setTextureCoords(source.left, source.top, source.right, source.bottom);
}
private void setTextureCoords(float left, float top,
float right, float bottom) {
mGL.glMatrixMode(GL11.GL_TEXTURE);
mTextureMatrixValues[0] = right - left;
mTextureMatrixValues[5] = bottom - top;
mTextureMatrixValues[10] = 1;
mTextureMatrixValues[12] = left;
mTextureMatrixValues[13] = top;
mTextureMatrixValues[15] = 1;
mGL.glLoadMatrixf(mTextureMatrixValues, 0);
mGL.glMatrixMode(GL11.GL_MODELVIEW);
}
private void setTextureCoords(float[] mTextureTransform) {
mGL.glMatrixMode(GL11.GL_TEXTURE);
mGL.glLoadMatrixf(mTextureTransform, 0);
mGL.glMatrixMode(GL11.GL_MODELVIEW);
}
// unloadTexture and deleteBuffer can be called from the finalizer thread,
// so we synchronized on the mUnboundTextures object.
@Override
public boolean unloadTexture(BasicTexture t) {
synchronized (mUnboundTextures) {
if (!t.isLoaded()) return false;
mUnboundTextures.add(t.mId);
return true;
}
}
@Override
public void deleteBuffer(int bufferId) {
synchronized (mUnboundTextures) {
mDeleteBuffers.add(bufferId);
}
}
@Override
public void deleteRecycledResources() {
synchronized (mUnboundTextures) {
IntArray ids = mUnboundTextures;
if (ids.size() > 0) {
mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0);
ids.clear();
}
ids = mDeleteBuffers;
if (ids.size() > 0) {
mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0);
ids.clear();
}
}
}
@Override
public void save() {
save(SAVE_FLAG_ALL);
}
@Override
public void save(int saveFlags) {
ConfigState config = obtainRestoreConfig();
if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
config.mAlpha = mAlpha;
} else {
config.mAlpha = -1;
}
if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
} else {
config.mMatrix[0] = Float.NEGATIVE_INFINITY;
}
mRestoreStack.add(config);
}
@Override
public void restore() {
if (mRestoreStack.isEmpty()) throw new IllegalStateException();
ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1);
config.restore(this);
freeRestoreConfig(config);
}
private void freeRestoreConfig(ConfigState action) {
action.mNextFree = mRecycledRestoreAction;
mRecycledRestoreAction = action;
}
private ConfigState obtainRestoreConfig() {
if (mRecycledRestoreAction != null) {
ConfigState result = mRecycledRestoreAction;
mRecycledRestoreAction = result.mNextFree;
return result;
}
return new ConfigState();
}
private static class ConfigState {
float mAlpha;
float mMatrix[] = new float[16];
ConfigState mNextFree;
public void restore(GLES11Canvas canvas) {
if (mAlpha >= 0) canvas.setAlpha(mAlpha);
if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
}
}
}
@Override
@SuppressLint("DefaultLocale")
public void dumpStatisticsAndClear() {
String line = String.format(
"MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d",
mCountDrawMesh, mCountTextureRect, mCountTextureOES,
mCountFillRect, mCountDrawLine);
mCountDrawMesh = 0;
mCountTextureRect = 0;
mCountTextureOES = 0;
mCountFillRect = 0;
mCountDrawLine = 0;
Log.d(TAG, line);
}
private void saveTransform() {
System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16);
}
private void restoreTransform() {
System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);
}
private void setRenderTarget(RawTexture texture) {
GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL;
if (mTargetTexture == null && texture != null) {
mGLId.glGenBuffers(1, mFrameBuffer, 0);
gl11ep.glBindFramebufferOES(
GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]);
}
if (mTargetTexture != null && texture == null) {
gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0);
gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0);
}
mTargetTexture = texture;
if (texture == null) {
setSize(mScreenWidth, mScreenHeight);
} else {
setSize(texture.getWidth(), texture.getHeight());
if (!texture.isLoaded()) texture.prepare(this);
gl11ep.glFramebufferTexture2DOES(
GL11ExtensionPack.GL_FRAMEBUFFER_OES,
GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,
GL11.GL_TEXTURE_2D, texture.getId(), 0);
checkFramebufferStatus(gl11ep);
}
}
@Override
public void endRenderTarget() {
RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1);
setRenderTarget(texture);
restore(); // restore matrix and alpha
}
@Override
public void beginRenderTarget(RawTexture texture) {
save(); // save matrix and alpha
mTargetStack.add(mTargetTexture);
setRenderTarget(texture);
}
private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) {
int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES);
if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) {
String msg = "";
switch (status) {
case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
msg = "FRAMEBUFFER_FORMATS";
break;
case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
msg = "FRAMEBUFFER_ATTACHMENT";
break;
case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
msg = "FRAMEBUFFER_MISSING_ATTACHMENT";
break;
case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES:
msg = "FRAMEBUFFER_DRAW_BUFFER";
break;
case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES:
msg = "FRAMEBUFFER_READ_BUFFER";
break;
case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES:
msg = "FRAMEBUFFER_UNSUPPORTED";
break;
case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
break;
}
throw new RuntimeException(msg + ":" + Integer.toHexString(status));
}
}
@Override
public void setTextureParameters(BasicTexture texture) {
int width = texture.getWidth();
int height = texture.getHeight();
// Define a vertically flipped crop rectangle for OES_draw_texture.
// The four values in sCropRect are: left, bottom, width, and
// height. Negative value of width or height means flip.
sCropRect[0] = 0;
sCropRect[1] = height;
sCropRect[2] = width;
sCropRect[3] = -height;
// Set texture parameters.
int target = texture.getTarget();
mGL.glBindTexture(target, texture.getId());
mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
}
@Override
public void initializeTextureSize(BasicTexture texture, int format, int type) {
int target = texture.getTarget();
mGL.glBindTexture(target, texture.getId());
int width = texture.getTextureWidth();
int height = texture.getTextureHeight();
mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null);
}
@Override
public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
int target = texture.getTarget();
mGL.glBindTexture(target, texture.getId());
GLUtils.texImage2D(target, 0, bitmap, 0);
}
@Override
public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,
int format, int type) {
int target = texture.getTarget();
mGL.glBindTexture(target, texture.getId());
GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
}
@Override
public int uploadBuffer(FloatBuffer buf) {
return uploadBuffer(buf, Float.SIZE / Byte.SIZE);
}
@Override
public int uploadBuffer(ByteBuffer buf) {
return uploadBuffer(buf, 1);
}
private int uploadBuffer(Buffer buf, int elementSize) {
int[] bufferIds = new int[1];
mGLId.glGenBuffers(bufferIds.length, bufferIds, 0);
int bufferId = bufferIds[0];
mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId);
mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf,
GL11.GL_STATIC_DRAW);
return bufferId;
}
@Override
public void recoverFromLightCycle() {
// This is only required for GLES20
}
@Override
public void getBounds(Rect bounds, int x, int y, int width, int height) {
// This is only required for GLES20
}
@Override
public GLId getGLId() {
return mGLId;
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11ExtensionPack;
/**
* Open GL ES 1.1 implementation for generating and destroying texture IDs and
* buffer IDs
*/
public class GLES11IdImpl implements GLId {
private static int sNextId = 1;
// Mutex for sNextId
private static Object sLock = new Object();
@Override
public int generateTexture() {
synchronized (sLock) {
return sNextId++;
}
}
@Override
public void glGenBuffers(int n, int[] buffers, int offset) {
synchronized (sLock) {
while (n-- > 0) {
buffers[offset + n] = sNextId++;
}
}
}
@Override
public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
synchronized (sLock) {
gl.glDeleteTextures(n, textures, offset);
}
}
@Override
public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
synchronized (sLock) {
gl.glDeleteBuffers(n, buffers, offset);
}
}
@Override
public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
synchronized (sLock) {
gl11ep.glDeleteFramebuffersOES(n, buffers, offset);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,42 @@
package com.hippo.ehviewer.gallery.glrenderer;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11ExtensionPack;
import android.opengl.GLES20;
public class GLES20IdImpl implements GLId {
private final int[] mTempIntArray = new int[1];
@Override
public int generateTexture() {
GLES20.glGenTextures(1, mTempIntArray, 0);
GLES20Canvas.checkError();
return mTempIntArray[0];
}
@Override
public void glGenBuffers(int n, int[] buffers, int offset) {
GLES20.glGenBuffers(n, buffers, offset);
GLES20Canvas.checkError();
}
@Override
public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
GLES20.glDeleteTextures(n, textures, offset);
GLES20Canvas.checkError();
}
@Override
public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
GLES20.glDeleteBuffers(n, buffers, offset);
GLES20Canvas.checkError();
}
@Override
public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
GLES20.glDeleteFramebuffers(n, buffers, offset);
GLES20Canvas.checkError();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11ExtensionPack;
// This mimics corresponding GL functions.
public interface GLId {
public int generateTexture();
public void glGenBuffers(int n, int[] buffers, int offset);
public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset);
public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset);
public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset);
}
@@ -0,0 +1,46 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import junit.framework.Assert;
public class GLPaint {
private float mLineWidth = 1f;
private int mColor = 0;
public GLPaint(int color, float lineWidth) {
setColor(color);
setLineWidth(lineWidth);
}
public void setColor(int color) {
mColor = color;
}
public int getColor() {
return mColor;
}
public void setLineWidth(float width) {
Assert.assertTrue(width >= 0);
mLineWidth = width;
}
public float getLineWidth() {
return mLineWidth;
}
}
@@ -0,0 +1,142 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.gallery.glrenderer;
import javax.microedition.khronos.opengles.GL11;
import android.opengl.GLES20;
import android.util.Log;
import com.hippo.ehviewer.gallery.image.Image;
public class ImageTexture extends BasicTexture implements Uploaded {
private static final String TAG = ImageTexture.class.getSimpleName();
private Image mImage;
private boolean mContentValid = true;
private boolean mOpaque = true;
public ImageTexture(Image image) {
mImage = image;
setSize(image.getWidth(), image.getHeight());
}
public void start() {
if (mImage != null)
mImage.start();
}
public void stop() {
if (mImage != null)
mImage.stop();
}
public boolean isAnimated() {
if (mImage == null || mImage.isRecycled())
return false;
else
return mImage.isAnimated();
}
@Override
public boolean isOpaque() {
return mOpaque;
}
public void setOpaque(boolean opaque) {
mOpaque = opaque;
}
/**
* Whether the content on GPU is valid.
*/
@Override
public boolean isContentValid() {
return isLoaded() && mContentValid;
}
@Override
protected int getTarget() {
return GL11.GL_TEXTURE_2D;
}
public int getFormat() {
return mImage.getFormat();
}
public int getType() {
return mImage.getType();
}
public static void checkError() {
int error = GLES20.glGetError();
if (error != 0) {
Throwable t = new Throwable();
Log.e(TAG, "GL error: " + error, t);
}
}
private void uploadToCanvas(GLCanvas canvas) {
mId = canvas.getGLId().generateTexture();
int format = getFormat();
int type = getType();
canvas.setTextureParameters(this);
canvas.initializeTextureSize(this, format, type);
GLES20.glBindTexture(getTarget(), mId);
checkError();
mImage.render();
setAssociatedCanvas(canvas);
mState = STATE_LOADED;
mContentValid = true;
}
@Override
public void updateContent(GLCanvas canvas) {
if (!isLoaded()) {
uploadToCanvas(canvas);
} else if (isAnimated()) {
GLES20.glBindTexture(getTarget(), mId);
checkError();
mImage.render();
mContentValid = true;
}
}
@Override
protected boolean onBind(GLCanvas canvas) {
if (mImage == null || mImage.isRecycled()) {
return false;
} else {
updateContent(canvas);
return isContentValid();
}
}
@Override
public void recycle() {
super.recycle();
if (mImage != null) {
mImage.recycle();
mImage = null;
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.gallery.glrenderer;
import junit.framework.Assert;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Movie;
public class MovieTexture extends CanvasTexture {
private static final int DEFAULT_MOVIE_DURATION = 1000;
private final Movie mMovie;
private final int mDuration;
private final long mMovieStart;
private MovieTexture(Movie movie, int width, int height) {
super(width, height);
mMovie = movie;
int dur = mMovie.duration();
if (dur == 0)
mDuration = DEFAULT_MOVIE_DURATION;
else
mDuration = dur;
mMovieStart = System.currentTimeMillis();
}
public static MovieTexture newInstance(Movie movie) {
Assert.assertTrue(movie != null);
return new MovieTexture(movie, movie.width(), movie.height());
}
@Override
protected boolean onBind(GLCanvas canvas) {
invalidateContent();
setSize(mMovie.width(), mMovie.height());
return super.onBind(canvas);
}
@Override
protected void onDraw(Canvas canvas, Bitmap backing) {
mMovie.setTime((int)((System.currentTimeMillis() - mMovieStart) % mDuration));
mMovie.draw(canvas, 0, 0);
}
}
@@ -0,0 +1,52 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
// MultiLineTexture is a texture shows the content of a specified String.
//
// To create a MultiLineTexture, use the newInstance() method and specify
// the String, the font size, and the color.
class MultiLineTexture extends CanvasTexture {
private final Layout mLayout;
private MultiLineTexture(Layout layout) {
super(layout.getWidth(), layout.getHeight());
mLayout = layout;
}
public static MultiLineTexture newInstance(
String text, int maxWidth, float textSize, int color,
Layout.Alignment alignment) {
TextPaint paint = StringTexture.getDefaultPaint(textSize, color);
Layout layout = new StaticLayout(text, 0, text.length(), paint,
maxWidth, alignment, 1, 0, true, null, 0);
return new MultiLineTexture(layout);
}
@Override
protected void onDraw(Canvas canvas, Bitmap backing) {
mLayout.draw(canvas);
}
}
@@ -0,0 +1,82 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import android.graphics.Rect;
// See "frameworks/base/include/utils/ResourceTypes.h" for the format of
// NinePatch chunk.
class NinePatchChunk {
public static final int NO_COLOR = 0x00000001;
public static final int TRANSPARENT_COLOR = 0x00000000;
public Rect mPaddings = new Rect();
public int mDivX[];
public int mDivY[];
public int mColor[];
private static void readIntArray(int[] data, ByteBuffer buffer) {
for (int i = 0, n = data.length; i < n; ++i) {
data[i] = buffer.getInt();
}
}
private static void checkDivCount(int length) {
if (length == 0 || (length & 0x01) != 0) {
throw new RuntimeException("invalid nine-patch: " + length);
}
}
public static NinePatchChunk deserialize(byte[] data) {
ByteBuffer byteBuffer =
ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
byte wasSerialized = byteBuffer.get();
if (wasSerialized == 0) return null;
NinePatchChunk chunk = new NinePatchChunk();
chunk.mDivX = new int[byteBuffer.get()];
chunk.mDivY = new int[byteBuffer.get()];
chunk.mColor = new int[byteBuffer.get()];
checkDivCount(chunk.mDivX.length);
checkDivCount(chunk.mDivY.length);
// skip 8 bytes
byteBuffer.getInt();
byteBuffer.getInt();
chunk.mPaddings.left = byteBuffer.getInt();
chunk.mPaddings.right = byteBuffer.getInt();
chunk.mPaddings.top = byteBuffer.getInt();
chunk.mPaddings.bottom = byteBuffer.getInt();
// skip 4 bytes
byteBuffer.getInt();
readIntArray(chunk.mDivX, byteBuffer);
readIntArray(chunk.mDivY, byteBuffer);
readIntArray(chunk.mColor, byteBuffer);
return chunk;
}
}
@@ -0,0 +1,425 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import com.hippo.ehviewer.util.MathUtils;
// NinePatchTexture is a texture backed by a NinePatch resource.
//
// getPaddings() returns paddings specified in the NinePatch.
// getNinePatchChunk() returns the layout data specified in the NinePatch.
//
public class NinePatchTexture extends ResourceTexture {
@SuppressWarnings("unused")
private static final String TAG = "NinePatchTexture";
private NinePatchChunk mChunk;
private final SmallCache<NinePatchInstance> mInstanceCache
= new SmallCache<NinePatchInstance>();
public NinePatchTexture(Context context, int resId) {
super(context, resId);
}
@Override
protected Bitmap onGetBitmap() {
if (mBitmap != null) return mBitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = BitmapFactory.decodeResource(
mContext.getResources(), mResId, options);
mBitmap = bitmap;
setSize(bitmap.getWidth(), bitmap.getHeight());
byte[] chunkData = bitmap.getNinePatchChunk();
mChunk = chunkData == null
? null
: NinePatchChunk.deserialize(bitmap.getNinePatchChunk());
if (mChunk == null) {
throw new RuntimeException("invalid nine-patch image: " + mResId);
}
return bitmap;
}
public Rect getPaddings() {
// get the paddings from nine patch
if (mChunk == null) onGetBitmap();
return mChunk.mPaddings;
}
public NinePatchChunk getNinePatchChunk() {
if (mChunk == null) onGetBitmap();
return mChunk;
}
// This is a simple cache for a small number of things. Linear search
// is used because the cache is small. It also tries to remove less used
// item when the cache is full by moving the often-used items to the front.
private static class SmallCache<V> {
private static final int CACHE_SIZE = 16;
private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
private final int[] mKey = new int[CACHE_SIZE];
@SuppressWarnings("unchecked")
private final V[] mValue = (V[]) new Object[CACHE_SIZE];
private int mCount; // number of items in this cache
// Puts a value into the cache. If the cache is full, also returns
// a less used item, otherwise returns null.
public V put(int key, V value) {
if (mCount == CACHE_SIZE) {
V old = mValue[CACHE_SIZE - 1]; // remove the last item
mKey[CACHE_SIZE - 1] = key;
mValue[CACHE_SIZE - 1] = value;
return old;
} else {
mKey[mCount] = key;
mValue[mCount] = value;
mCount++;
return null;
}
}
public V get(int key) {
for (int i = 0; i < mCount; i++) {
if (mKey[i] == key) {
// Move the accessed item one position to the front, so it
// will less likely to be removed when cache is full. Only
// do this if the cache is starting to get full.
if (mCount > CACHE_SIZE_START_MOVE && i > 0) {
int tmpKey = mKey[i];
mKey[i] = mKey[i - 1];
mKey[i - 1] = tmpKey;
V tmpValue = mValue[i];
mValue[i] = mValue[i - 1];
mValue[i - 1] = tmpValue;
}
return mValue[i];
}
}
return null;
}
public void clear() {
for (int i = 0; i < mCount; i++) {
mValue[i] = null; // make sure it's can be garbage-collected.
}
mCount = 0;
}
public int size() {
return mCount;
}
public V valueAt(int i) {
return mValue[i];
}
}
private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
int key = w;
key = (key << 16) | h;
NinePatchInstance instance = mInstanceCache.get(key);
if (instance == null) {
instance = new NinePatchInstance(this, w, h);
NinePatchInstance removed = mInstanceCache.put(key, instance);
if (removed != null) {
removed.recycle(canvas);
}
}
return instance;
}
@Override
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
if (!isLoaded()) {
mInstanceCache.clear();
}
if (w != 0 && h != 0) {
findInstance(canvas, w, h).draw(canvas, this, x, y);
}
}
@Override
public void recycle() {
super.recycle();
GLCanvas canvas = mCanvasRef;
if (canvas == null) return;
int n = mInstanceCache.size();
for (int i = 0; i < n; i++) {
NinePatchInstance instance = mInstanceCache.valueAt(i);
instance.recycle(canvas);
}
mInstanceCache.clear();
}
}
// This keeps data for a specialization of NinePatchTexture with the size
// (width, height). We pre-compute the coordinates for efficiency.
class NinePatchInstance {
@SuppressWarnings("unused")
private static final String TAG = "NinePatchInstance";
// We need 16 vertices for a normal nine-patch image (the 4x4 vertices)
private static final int VERTEX_BUFFER_SIZE = 16 * 2;
// We need 22 indices for a normal nine-patch image, plus 2 for each
// transparent region. Current there are at most 1 transparent region.
private static final int INDEX_BUFFER_SIZE = 22 + 2;
private FloatBuffer mXyBuffer;
private FloatBuffer mUvBuffer;
private ByteBuffer mIndexBuffer;
// Names for buffer names: xy, uv, index.
private int mXyBufferName = -1;
private int mUvBufferName;
private int mIndexBufferName;
private int mIdxCount;
public NinePatchInstance(NinePatchTexture tex, int width, int height) {
NinePatchChunk chunk = tex.getNinePatchChunk();
if (width <= 0 || height <= 0) {
throw new RuntimeException("invalid dimension");
}
// The code should be easily extended to handle the general cases by
// allocating more space for buffers. But let's just handle the only
// use case.
if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) {
throw new RuntimeException("unsupported nine patch");
}
float divX[] = new float[4];
float divY[] = new float[4];
float divU[] = new float[4];
float divV[] = new float[4];
int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor);
}
/**
* Stretches the texture according to the nine-patch rules. It will
* linearly distribute the strechy parts defined in the nine-patch chunk to
* the target area.
*
* <pre>
* source
* /--------------^---------------\
* u0 u1 u2 u3 u4 u5
* div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
* | div0 div1 div2 div3 |
* | | / / / /
* | | / / / /
* | | / / / /
* |fffff|ssss|fff|sss|ffff| ---> x
* x0 x1 x2 x3 x4 x5
* \----------v------------/
* target
*
* f: fixed segment
* s: stretchy segment
* </pre>
*
* @param div the stretch parts defined in nine-patch chunk
* @param source the length of the texture
* @param target the length on the drawing plan
* @param u output, the positions of these dividers in the texture
* coordinate
* @param x output, the corresponding position of these dividers on the
* drawing plan
* @return the number of these dividers.
*/
private static int stretch(
float x[], float u[], int div[], int source, int target) {
int textureSize = MathUtils.nextPowerOf2(source);
float textureBound = (float) source / textureSize;
float stretch = 0;
for (int i = 0, n = div.length; i < n; i += 2) {
stretch += div[i + 1] - div[i];
}
float remaining = target - source + stretch;
float lastX = 0;
float lastU = 0;
x[0] = 0;
u[0] = 0;
for (int i = 0, n = div.length; i < n; i += 2) {
// Make the stretchy segment a little smaller to prevent sampling
// on neighboring fixed segments.
// fixed segment
x[i + 1] = lastX + (div[i] - lastU) + 0.5f;
u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound);
// stretchy segment
float partU = div[i + 1] - div[i];
float partX = remaining * partU / stretch;
remaining -= partX;
stretch -= partU;
lastX = x[i + 1] + partX;
lastU = div[i + 1];
x[i + 2] = lastX - 0.5f;
u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound);
}
// the last fixed segment
x[div.length + 1] = target;
u[div.length + 1] = textureBound;
// remove segments with length 0.
int last = 0;
for (int i = 1, n = div.length + 2; i < n; ++i) {
if ((x[i] - x[last]) < 1f) continue;
x[++last] = x[i];
u[last] = u[i];
}
return last + 1;
}
private void prepareVertexData(float x[], float y[], float u[], float v[],
int nx, int ny, int[] color) {
/*
* Given a 3x3 nine-patch image, the vertex order is defined as the
* following graph:
*
* (0) (1) (2) (3)
* | /| /| /|
* | / | / | / |
* (4) (5) (6) (7)
* | \ | \ | \ |
* | \| \| \|
* (8) (9) (A) (B)
* | /| /| /|
* | / | / | / |
* (C) (D) (E) (F)
*
* And we draw the triangle strip in the following index order:
*
* index: 04152637B6A5948C9DAEBF
*/
int pntCount = 0;
float xy[] = new float[VERTEX_BUFFER_SIZE];
float uv[] = new float[VERTEX_BUFFER_SIZE];
for (int j = 0; j < ny; ++j) {
for (int i = 0; i < nx; ++i) {
int xIndex = (pntCount++) << 1;
int yIndex = xIndex + 1;
xy[xIndex] = x[i];
xy[yIndex] = y[j];
uv[xIndex] = u[i];
uv[yIndex] = v[j];
}
}
int idxCount = 1;
boolean isForward = false;
byte index[] = new byte[INDEX_BUFFER_SIZE];
for (int row = 0; row < ny - 1; row++) {
--idxCount;
isForward = !isForward;
int start, end, inc;
if (isForward) {
start = 0;
end = nx;
inc = 1;
} else {
start = nx - 1;
end = -1;
inc = -1;
}
for (int col = start; col != end; col += inc) {
int k = row * nx + col;
if (col != start) {
int colorIdx = row * (nx - 1) + col;
if (isForward) colorIdx--;
if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) {
index[idxCount] = index[idxCount - 1];
++idxCount;
index[idxCount++] = (byte) k;
}
}
index[idxCount++] = (byte) k;
index[idxCount++] = (byte) (k + nx);
}
}
mIdxCount = idxCount;
int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE);
mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount);
mXyBuffer.put(xy, 0, pntCount * 2).position(0);
mUvBuffer.put(uv, 0, pntCount * 2).position(0);
mIndexBuffer.put(index, 0, idxCount).position(0);
}
private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
}
private void prepareBuffers(GLCanvas canvas) {
mXyBufferName = canvas.uploadBuffer(mXyBuffer);
mUvBufferName = canvas.uploadBuffer(mUvBuffer);
mIndexBufferName = canvas.uploadBuffer(mIndexBuffer);
// These buffers are never used again.
mXyBuffer = null;
mUvBuffer = null;
mIndexBuffer = null;
}
public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) {
if (mXyBufferName == -1) {
prepareBuffers(canvas);
}
canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount);
}
public void recycle(GLCanvas canvas) {
if (mXyBuffer == null) {
canvas.deleteBuffer(mXyBufferName);
canvas.deleteBuffer(mUvBufferName);
canvas.deleteBuffer(mIndexBufferName);
mXyBufferName = -1;
}
}
}
@@ -0,0 +1,73 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import javax.microedition.khronos.opengles.GL11;
import android.util.Log;
public class RawTexture extends BasicTexture {
private static final String TAG = "RawTexture";
private final boolean mOpaque;
private boolean mIsFlipped;
public RawTexture(int width, int height, boolean opaque) {
mOpaque = opaque;
setSize(width, height);
}
@Override
public boolean isOpaque() {
return mOpaque;
}
@Override
public boolean isFlippedVertically() {
return mIsFlipped;
}
public void setIsFlippedVertically(boolean isFlipped) {
mIsFlipped = isFlipped;
}
protected void prepare(GLCanvas canvas) {
GLId glId = canvas.getGLId();
mId = glId.generateTexture();
canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE);
canvas.setTextureParameters(this);
mState = STATE_LOADED;
setAssociatedCanvas(canvas);
}
@Override
protected boolean onBind(GLCanvas canvas) {
if (isLoaded()) return true;
Log.w(TAG, "lost the content due to context change");
return false;
}
@Override
public void yield() {
// we cannot free the texture because we have no backup.
}
@Override
protected int getTarget() {
return GL11.GL_TEXTURE_2D;
}
}
@@ -0,0 +1,52 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import junit.framework.Assert;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
// ResourceTexture is a texture whose Bitmap is decoded from a resource.
// By default ResourceTexture is not opaque.
public class ResourceTexture extends UploadedTexture {
protected final Context mContext;
protected final int mResId;
public ResourceTexture(Context context, int resId) {
Assert.assertNotNull(context);
mContext = context;
mResId = resId;
setOpaque(false);
}
@Override
protected Bitmap onGetBitmap() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeResource(
mContext.getResources(), mResId, options);
}
@Override
protected void onFreeBitmap(Bitmap bitmap) {
if (!inFinalizer()) {
bitmap.recycle();
}
}
}
@@ -0,0 +1,87 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.TextUtils;
// StringTexture is a texture shows the content of a specified String.
//
// To create a StringTexture, use the newInstance() method and specify
// the String, the font size, and the color.
public class StringTexture extends CanvasTexture {
private final String mText;
private final TextPaint mPaint;
private final FontMetricsInt mMetrics;
private StringTexture(String text, TextPaint paint,
FontMetricsInt metrics, int width, int height) {
super(width, height);
mText = text;
mPaint = paint;
mMetrics = metrics;
}
public static TextPaint getDefaultPaint(float textSize, int color) {
TextPaint paint = new TextPaint();
paint.setTextSize(textSize);
paint.setAntiAlias(true);
paint.setColor(color);
paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);
return paint;
}
public static StringTexture newInstance(
String text, float textSize, int color) {
return newInstance(text, getDefaultPaint(textSize, color));
}
public static StringTexture newInstance(
String text, float textSize, int color,
float lengthLimit, boolean isBold) {
TextPaint paint = getDefaultPaint(textSize, color);
if (isBold) {
paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
}
if (lengthLimit > 0) {
text = TextUtils.ellipsize(
text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
}
return newInstance(text, paint);
}
private static StringTexture newInstance(String text, TextPaint paint) {
FontMetricsInt metrics = paint.getFontMetricsInt();
int width = (int)Math.ceil(paint.measureText(text));
int height = metrics.bottom - metrics.top;
// The texture size needs to be at least 1x1.
if (width <= 0) width = 1;
if (height <= 0) height = 1;
return new StringTexture(text, paint, metrics, width, height);
}
@Override
protected void onDraw(Canvas canvas, Bitmap backing) {
canvas.translate(0, -mMetrics.ascent);
canvas.drawText(mText, 0, 0, mPaint);
}
}
@@ -0,0 +1,44 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
// Texture is a rectangular image which can be drawn on GLCanvas.
// The isOpaque() function gives a hint about whether the texture is opaque,
// so the drawing can be done faster.
//
// This is the current texture hierarchy:
//
// Texture
// -- ColorTexture
// -- FadeInTexture
// -- BasicTexture
// -- UploadedTexture
// -- BitmapTexture
// -- Tile
// -- ResourceTexture
// -- NinePatchTexture
// -- CanvasTexture
// -- StringTexture
//
public interface Texture {
public int getWidth();
public int getHeight();
public void draw(GLCanvas canvas, int x, int y);
public void draw(GLCanvas canvas, int x, int y, int w, int h);
public boolean isOpaque();
}
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
import java.util.ArrayDeque;
import com.hippo.ehviewer.gallery.ui.GLRoot;
import com.hippo.ehviewer.gallery.ui.GLRoot.OnGLIdleListener;
public class TextureUploader implements OnGLIdleListener {
private static final int INIT_CAPACITY = 64;
private static final int QUOTA_PER_FRAME = 1;
private final ArrayDeque<UploadedTexture> mFgTextures =
new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
private final ArrayDeque<UploadedTexture> mBgTextures =
new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
private final GLRoot mGLRoot;
private volatile boolean mIsQueued = false;
public TextureUploader(GLRoot root) {
mGLRoot = root;
}
public synchronized void clear() {
while (!mFgTextures.isEmpty()) {
mFgTextures.pop().setIsUploading(false);
}
while (!mBgTextures.isEmpty()) {
mBgTextures.pop().setIsUploading(false);
}
}
// caller should hold synchronized on "this"
private void queueSelfIfNeed() {
if (mIsQueued) return;
mIsQueued = true;
mGLRoot.addOnGLIdleListener(this);
}
public synchronized void addBgTexture(UploadedTexture t) {
if (t.isContentValid()) return;
mBgTextures.addLast(t);
t.setIsUploading(true);
queueSelfIfNeed();
}
public synchronized void addFgTexture(UploadedTexture t) {
if (t.isContentValid()) return;
mFgTextures.addLast(t);
t.setIsUploading(true);
queueSelfIfNeed();
}
private int upload(GLCanvas canvas, ArrayDeque<UploadedTexture> deque,
int uploadQuota, boolean isBackground) {
while (uploadQuota > 0) {
UploadedTexture t;
synchronized (this) {
if (deque.isEmpty()) break;
t = deque.removeFirst();
t.setIsUploading(false);
if (t.isContentValid()) continue;
// this has to be protected by the synchronized block
// to prevent the inner bitmap get recycled
t.updateContent(canvas);
}
// It will took some more time for a texture to be drawn for
// the first time.
// Thus, when scrolling, if a new column appears on screen,
// it may cause a UI jank even these textures are uploaded.
if (isBackground) t.draw(canvas, 0, 0);
--uploadQuota;
}
return uploadQuota;
}
@Override
public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
int uploadQuota = QUOTA_PER_FRAME;
uploadQuota = upload(canvas, mFgTextures, uploadQuota, false);
if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender();
upload(canvas, mBgTextures, uploadQuota, true);
synchronized (this) {
mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty();
return mIsQueued;
}
}
}
@@ -0,0 +1,356 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.glrenderer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.os.SystemClock;
import com.hippo.ehviewer.gallery.ui.GLRoot;
import com.hippo.ehviewer.gallery.ui.GLRoot.OnGLIdleListener;
// This class is similar to BitmapTexture, except the bitmap is
// split into tiles. By doing so, we may increase the time required to
// upload the whole bitmap but we reduce the time of uploading each tile
// so it make the animation more smooth and prevents jank.
public class TiledTexture implements Texture {
private static final int CONTENT_SIZE = 254;
private static final int BORDER_SIZE = 1;
private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;
private static final int INIT_CAPACITY = 8;
// We are targeting at 60fps, so we have 16ms for each frame.
// In this 16ms, we use about 4~8 ms to upload tiles.
private static final long UPLOAD_TILE_LIMIT = 4; // ms
private static Tile sFreeTileHead = null;
private static final Object sFreeTileLock = new Object();
private static Bitmap sUploadBitmap;
private static Canvas sCanvas;
private static Paint sBitmapPaint;
private static Paint sPaint;
private int mUploadIndex = 0;
private final Tile[] mTiles; // Can be modified in different threads.
// Should be protected by "synchronized."
private final int mWidth;
private final int mHeight;
private final RectF mSrcRect = new RectF();
private final RectF mDestRect = new RectF();
public static class Uploader implements OnGLIdleListener {
private final ArrayDeque<TiledTexture> mTextures =
new ArrayDeque<TiledTexture>(INIT_CAPACITY);
private final GLRoot mGlRoot;
private boolean mIsQueued = false;
public Uploader(GLRoot glRoot) {
mGlRoot = glRoot;
}
public synchronized void clear() {
mTextures.clear();
}
public synchronized void addTexture(TiledTexture t) {
if (t.isReady()) return;
mTextures.addLast(t);
if (mIsQueued) return;
mIsQueued = true;
mGlRoot.addOnGLIdleListener(this);
}
@Override
public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
ArrayDeque<TiledTexture> deque = mTextures;
synchronized (this) {
long now = SystemClock.uptimeMillis();
long dueTime = now + UPLOAD_TILE_LIMIT;
while (now < dueTime && !deque.isEmpty()) {
TiledTexture t = deque.peekFirst();
if (t.uploadNextTile(canvas)) {
deque.removeFirst();
mGlRoot.requestRender();
}
now = SystemClock.uptimeMillis();
}
mIsQueued = !mTextures.isEmpty();
// return true to keep this listener in the queue
return mIsQueued;
}
}
}
private static class Tile extends UploadedTexture {
public int offsetX;
public int offsetY;
public Bitmap bitmap;
public Tile nextFreeTile;
public int contentWidth;
public int contentHeight;
@Override
public void setSize(int width, int height) {
contentWidth = width;
contentHeight = height;
mWidth = width + 2 * BORDER_SIZE;
mHeight = height + 2 * BORDER_SIZE;
mTextureWidth = TILE_SIZE;
mTextureHeight = TILE_SIZE;
}
@Override
protected Bitmap onGetBitmap() {
// make a local copy of the reference to the bitmap,
// since it might be null'd in a different thread. b/8694871
Bitmap localBitmapRef = bitmap;
bitmap = null;
if (localBitmapRef != null) {
int x = BORDER_SIZE - offsetX;
int y = BORDER_SIZE - offsetY;
int r = localBitmapRef.getWidth() + x;
int b = localBitmapRef.getHeight() + y;
sCanvas.drawBitmap(localBitmapRef, x, y, sBitmapPaint);
localBitmapRef = null;
// draw borders if need
if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);
if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);
if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);
if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);
}
return sUploadBitmap;
}
@Override
protected void onFreeBitmap(Bitmap bitmap) {
// do nothing
}
}
private static void freeTile(Tile tile) {
tile.invalidateContent();
tile.bitmap = null;
synchronized (sFreeTileLock) {
tile.nextFreeTile = sFreeTileHead;
sFreeTileHead = tile;
}
}
private static Tile obtainTile() {
synchronized (sFreeTileLock) {
Tile result = sFreeTileHead;
if (result == null) return new Tile();
sFreeTileHead = result.nextFreeTile;
result.nextFreeTile = null;
return result;
}
}
private boolean uploadNextTile(GLCanvas canvas) {
if (mUploadIndex == mTiles.length) return true;
synchronized (mTiles) {
Tile next = mTiles[mUploadIndex++];
// Make sure tile has not already been recycled by the time
// this is called (race condition in onGLIdle)
if (next.bitmap != null) {
boolean hasBeenLoad = next.isLoaded();
next.updateContent(canvas);
// It will take some time for a texture to be drawn for the first
// time. When scrolling, we need to draw several tiles on the screen
// at the same time. It may cause a UI jank even these textures has
// been uploaded.
if (!hasBeenLoad) next.draw(canvas, 0, 0);
}
}
return mUploadIndex == mTiles.length;
}
public TiledTexture(Bitmap bitmap) {
mWidth = bitmap.getWidth();
mHeight = bitmap.getHeight();
ArrayList<Tile> list = new ArrayList<Tile>();
for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) {
for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) {
Tile tile = obtainTile();
tile.offsetX = x;
tile.offsetY = y;
tile.bitmap = bitmap;
tile.setSize(
Math.min(CONTENT_SIZE, mWidth - x),
Math.min(CONTENT_SIZE, mHeight - y));
list.add(tile);
}
}
mTiles = list.toArray(new Tile[list.size()]);
}
public boolean isReady() {
return mUploadIndex == mTiles.length;
}
// Can be called in UI thread.
public void recycle() {
synchronized (mTiles) {
for (int i = 0, n = mTiles.length; i < n; ++i) {
freeTile(mTiles[i]);
}
}
}
public static void freeResources() {
sUploadBitmap = null;
sCanvas = null;
sBitmapPaint = null;
sPaint = null;
}
public static void prepareResources() {
sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888);
sCanvas = new Canvas(sUploadBitmap);
sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
sPaint = new Paint();
sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
sPaint.setColor(Color.TRANSPARENT);
}
// We want to draw the "source" on the "target".
// This method is to find the "output" rectangle which is
// the corresponding area of the "src".
// (x,y) target
// (x0,y0) source +---------------+
// +----------+ | |
// | src | | output |
// | +--+ | linear map | +----+ |
// | +--+ | ----------> | | | |
// | | by (scaleX, scaleY) | +----+ |
// +----------+ | |
// Texture +---------------+
// Canvas
private static void mapRect(RectF output,
RectF src, float x0, float y0, float x, float y, float scaleX,
float scaleY) {
output.set(x + (src.left - x0) * scaleX,
y + (src.top - y0) * scaleY,
x + (src.right - x0) * scaleX,
y + (src.bottom - y0) * scaleY);
}
// Draws a mixed color of this texture and a specified color onto the
// a rectangle. The used color is: from * (1 - ratio) + to * ratio.
public void drawMixed(GLCanvas canvas, int color, float ratio,
int x, int y, int width, int height) {
RectF src = mSrcRect;
RectF dest = mDestRect;
float scaleX = (float) width / mWidth;
float scaleY = (float) height / mHeight;
synchronized (mTiles) {
for (int i = 0, n = mTiles.length; i < n; ++i) {
Tile t = mTiles[i];
src.set(0, 0, t.contentWidth, t.contentHeight);
src.offset(t.offsetX, t.offsetY);
mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);
}
}
}
// Draws the texture on to the specified rectangle.
@Override
public void draw(GLCanvas canvas, int x, int y, int width, int height) {
RectF src = mSrcRect;
RectF dest = mDestRect;
float scaleX = (float) width / mWidth;
float scaleY = (float) height / mHeight;
synchronized (mTiles) {
for (int i = 0, n = mTiles.length; i < n; ++i) {
Tile t = mTiles[i];
src.set(0, 0, t.contentWidth, t.contentHeight);
src.offset(t.offsetX, t.offsetY);
mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
canvas.drawTexture(t, mSrcRect, mDestRect);
}
}
}
// Draws a sub region of this texture on to the specified rectangle.
public void draw(GLCanvas canvas, RectF source, RectF target) {
RectF src = mSrcRect;
RectF dest = mDestRect;
float x0 = source.left;
float y0 = source.top;
float x = target.left;
float y = target.top;
float scaleX = target.width() / source.width();
float scaleY = target.height() / source.height();
synchronized (mTiles) {
for (int i = 0, n = mTiles.length; i < n; ++i) {
Tile t = mTiles[i];
src.set(0, 0, t.contentWidth, t.contentHeight);
src.offset(t.offsetX, t.offsetY);
if (!src.intersect(source)) continue;
mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);
src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
canvas.drawTexture(t, src, dest);
}
}
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
@Override
public void draw(GLCanvas canvas, int x, int y) {
draw(canvas, x, y, mWidth, mHeight);
}
@Override
public boolean isOpaque() {
return false;
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.gallery.glrenderer;
public interface Uploaded {
public boolean isContentValid();
public void updateContent(GLCanvas canvas);
}
@@ -0,0 +1,297 @@
/*
* 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.hippo.ehviewer.gallery.glrenderer;
import java.util.HashMap;
import javax.microedition.khronos.opengles.GL11;
import junit.framework.Assert;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.opengl.GLUtils;
// UploadedTextures use a Bitmap for the content of the texture.
//
// Subclasses should implement onGetBitmap() to provide the Bitmap and
// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
// is not needed anymore.
//
// isContentValid() is meaningful only when the isLoaded() returns true.
// It means whether the content needs to be updated.
//
// The user of this class should call recycle() when the texture is not
// needed anymore.
//
// By default an UploadedTexture is opaque (so it can be drawn faster without
// blending). The user or subclass can override it using setOpaque().
public abstract class UploadedTexture extends BasicTexture implements Uploaded {
// To prevent keeping allocation the borders, we store those used borders here.
// Since the length will be power of two, it won't use too much memory.
private static HashMap<BorderKey, Bitmap> sBorderLines =
new HashMap<BorderKey, Bitmap>();
private static BorderKey sBorderKey = new BorderKey();
@SuppressWarnings("unused")
private static final String TAG = "Texture";
private boolean mContentValid = true;
// indicate this textures is being uploaded in background
private boolean mIsUploading = false;
private boolean mOpaque = true;
private boolean mThrottled = false;
private static int sUploadedCount;
private static final int UPLOAD_LIMIT = 100;
protected Bitmap mBitmap;
private int mBorder;
protected UploadedTexture() {
this(false);
}
protected UploadedTexture(boolean hasBorder) {
super(null, 0, STATE_UNLOADED);
if (hasBorder) {
setBorder(true);
mBorder = 1;
}
}
protected void setIsUploading(boolean uploading) {
mIsUploading = uploading;
}
public boolean isUploading() {
return mIsUploading;
}
private static class BorderKey implements Cloneable {
public boolean vertical;
public Config config;
public int length;
@Override
public int hashCode() {
int x = config.hashCode() ^ length;
return vertical ? x : -x;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof BorderKey)) return false;
BorderKey o = (BorderKey) object;
return vertical == o.vertical
&& config == o.config && length == o.length;
}
@Override
public BorderKey clone() {
try {
return (BorderKey) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
protected void setThrottled(boolean throttled) {
mThrottled = throttled;
}
private static Bitmap getBorderLine(
boolean vertical, Config config, int length) {
BorderKey key = sBorderKey;
key.vertical = vertical;
key.config = config;
key.length = length;
Bitmap bitmap = sBorderLines.get(key);
if (bitmap == null) {
bitmap = vertical
? Bitmap.createBitmap(1, length, config)
: Bitmap.createBitmap(length, 1, config);
sBorderLines.put(key.clone(), bitmap);
}
return bitmap;
}
private Bitmap getBitmap() {
if (mBitmap == null) {
mBitmap = onGetBitmap();
int w = mBitmap.getWidth() + mBorder * 2;
int h = mBitmap.getHeight() + mBorder * 2;
if (mWidth == UNSPECIFIED) {
setSize(w, h);
}
}
return mBitmap;
}
private void freeBitmap() {
Assert.assertTrue(mBitmap != null);
onFreeBitmap(mBitmap);
mBitmap = null;
}
@Override
public int getWidth() {
if (mWidth == UNSPECIFIED) getBitmap();
return mWidth;
}
@Override
public int getHeight() {
if (mWidth == UNSPECIFIED) getBitmap();
return mHeight;
}
protected abstract Bitmap onGetBitmap();
protected abstract void onFreeBitmap(Bitmap bitmap);
protected void invalidateContent() {
if (mBitmap != null) freeBitmap();
mContentValid = false;
mWidth = UNSPECIFIED;
mHeight = UNSPECIFIED;
}
/**
* Whether the content on GPU is valid.
*/
@Override
public boolean isContentValid() {
return isLoaded() && mContentValid;
}
/**
* Updates the content on GPU's memory.
* @param canvas
*/
@Override
public void updateContent(GLCanvas canvas) {
if (!isLoaded()) {
if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
return;
}
uploadToCanvas(canvas);
} else if (!mContentValid) {
Bitmap bitmap = getBitmap();
int format = GLUtils.getInternalFormat(bitmap);
int type = GLUtils.getType(bitmap);
canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
freeBitmap();
mContentValid = true;
}
}
public static void resetUploadLimit() {
sUploadedCount = 0;
}
public static boolean uploadLimitReached() {
return sUploadedCount > UPLOAD_LIMIT;
}
private void uploadToCanvas(GLCanvas canvas) {
Bitmap bitmap = getBitmap();
if (bitmap != null) {
try {
int bWidth = bitmap.getWidth();
int bHeight = bitmap.getHeight();
int texWidth = getTextureWidth();
int texHeight = getTextureHeight();
Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight);
// Upload the bitmap to a new texture.
mId = canvas.getGLId().generateTexture();
canvas.setTextureParameters(this);
if (bWidth == texWidth && bHeight == texHeight) {
canvas.initializeTexture(this, bitmap);
} else {
int format = GLUtils.getInternalFormat(bitmap);
int type = GLUtils.getType(bitmap);
Config config = bitmap.getConfig();
canvas.initializeTextureSize(this, format, type);
canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
if (mBorder > 0) {
// Left border
Bitmap line = getBorderLine(true, config, texHeight);
canvas.texSubImage2D(this, 0, 0, line, format, type);
// Top border
line = getBorderLine(false, config, texWidth);
canvas.texSubImage2D(this, 0, 0, line, format, type);
}
// Right border
if (mBorder + bWidth < texWidth) {
Bitmap line = getBorderLine(true, config, texHeight);
canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type);
}
// Bottom border
if (mBorder + bHeight < texHeight) {
Bitmap line = getBorderLine(false, config, texWidth);
canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type);
}
}
} finally {
freeBitmap();
}
// Update texture state.
setAssociatedCanvas(canvas);
mState = STATE_LOADED;
mContentValid = true;
} else {
mState = STATE_ERROR;
throw new RuntimeException("Texture load fail, no bitmap");
}
}
@Override
protected boolean onBind(GLCanvas canvas) {
updateContent(canvas);
return isContentValid();
}
@Override
protected int getTarget() {
return GL11.GL_TEXTURE_2D;
}
public void setOpaque(boolean isOpaque) {
mOpaque = isOpaque;
}
@Override
public boolean isOpaque() {
return mOpaque;
}
@Override
public void recycle() {
super.recycle();
if (mBitmap != null) freeBitmap();
}
}
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.gallery.image;
import com.hippo.ehviewer.util.Log;
public class GifImage extends Image {
private static final String TAG = Image.class.getSimpleName();
private final int mImageCount;
private final int[] mDelayArray;
private long mDelaySum;
private volatile long mStartTime;
private volatile long mStopTime = -1;
private volatile boolean mRunning = true;
private int mLastIndex = -1;
protected GifImage(long nativeImage, int fileFormat, int width, int height,
int format, int type, int[] delayArray) {
super(nativeImage, fileFormat, width, height, format, type);
mImageCount = delayArray.length;
mDelayArray = delayArray;
mDelaySum = 0;
for (int delay : delayArray)
mDelaySum += delay;
}
private int getCurIndex() {
int index = 0;
if (mRunning) {
if (mLastIndex == -1) {
index = 0;
mStartTime = System.currentTimeMillis();
} else {
long curTime = System.currentTimeMillis();
long time = (curTime - mStartTime) % mDelaySum;
int end = mImageCount - 1;
for (; index < end && (time -= mDelayArray[index]) > 0; index++)
;
}
return index;
} else {
return mLastIndex == -1 ? index : mLastIndex;
}
}
@Override
public void start() {
if (!mRunning) {
mRunning = true;
long curTime = System.currentTimeMillis();
mStartTime = curTime - (mStopTime == -1 ? curTime : mStopTime)
+ mStartTime;
}
}
@Override
public void stop() {
if (mRunning) {
mRunning = false;
mStopTime = System.currentTimeMillis();
}
}
@Override
public boolean isAnimated() {
return mImageCount > 1;
}
@Override
public void render() {
render(getCurIndex());
}
public void render(int index) {
if (mNativeImage != 0) {
if (index >= mImageCount || index < 0) {
Log.d(TAG, "GifImage try render index " + index + ", but image count is " + mImageCount);
return;
}
if (mLastIndex != index) {
nativeRender(mFormat, mType, mNativeImage, FILE_FORMAT_GIF,
index);
mLastIndex = index;
}
}
}
static {
System.loadLibrary("image");
}
private static native void nativeRender(int format, int type,
long nativeImage, int fileFormat, int index);
}
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2014 Hippo Seven
*
* 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.hippo.ehviewer.gallery.image;
import java.io.InputStream;
import javax.microedition.khronos.opengles.GL11;
public class Image {
private static final String TAG = Image.class.getSimpleName();
public static final int FILE_FORMAT_JPEG = 0x0;
public static final int FILE_FORMAT_PNG = 0x1;
public static final int FILE_FORMAT_BMP = 0x2;
public static final int FILE_FORMAT_GIF = 0x3;
public static final int FORMAT_AUTO = 0x0;
public static final int FORMAT_GRAY = GL11.GL_LUMINANCE;
public static final int FORMAT_GRAY_ALPHA = GL11.GL_LUMINANCE_ALPHA;
public static final int FORMAT_RGB = GL11.GL_RGB;
public static final int FORMAT_RGBA = GL11.GL_RGBA;
protected long mNativeImage;
protected final int mFileFormat;
protected final int mWidth;
protected final int mHeight;
protected final int mFormat;
protected final int mType;
protected Image(long nativeImage, int fileFormat, int width, int height,
int format, int type) {
mNativeImage = nativeImage;
mFileFormat = fileFormat;
mWidth = width;
mHeight = height;
mFormat = format;
mType = type;
}
public int getFileFormat() {
return mFileFormat;
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
public int getFormat() {
return mFormat;
}
public int getType() {
return mType;
}
public boolean isAnimated() {
return false;
}
public void start() {
// Empty
}
public void stop() {
// Empty
}
public void render() {
if (mNativeImage != 0) {
nativeRender(mFormat, mType, mNativeImage, mFileFormat);
}
}
public void recycle() {
if (mNativeImage != 0) {
nativeFree(mNativeImage, mFileFormat);
mNativeImage = 0;
}
}
public boolean isRecycled() {
return mNativeImage == 0;
}
/**
* format can be GL11.GL_RGB, GL11.GL_RGBA, GL11.GL_LUMINANCE,
* GL11.GL_LUMINANCE_ALPHA
*
* @param is
* @param format
* @return
*/
public static final Image decodeStream(InputStream is, int format) {
return nativeDecodeStream(is, format);
}
public static final Image decodeFile(String pathName, int format) {
return nativeDecodeFile(pathName, format);
}
static {
System.loadLibrary("image");
}
private static native Image nativeDecodeStream(InputStream is, int format);
private static native Image nativeDecodeFile(String pathName, int format);
private static native void nativeFree(long nativeImage, int format);
private static native void nativeRender(int format, int type,
long nativeImage, int fileFormat);
}
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery.ui;
import android.os.SystemClock;
//
// The animation time should ideally be the vsync time the frame will be
// displayed, but that is an unknown time in the future. So we use the system
// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called)
// as a approximation.
//
public class AnimationTime {
private static volatile long sTime;
// Sets current time as the animation time.
public static void update() {
sTime = SystemClock.uptimeMillis();
}
// Returns the animation time.
public static long get() {
return sTime;
}
public static long startTime() {
sTime = SystemClock.uptimeMillis();
return sTime;
}
}
Loaded 100 of 658 files, more files were not shown because too many files have changed in this diff. Show more