mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Image improvements (#53)
* ImageView loads Bitmaps in Java so that they can be GC'ed. Added ImageCache, AsyncLoading, Memory & DiskCache, Reusing Bimtaps * Change FILE_PREFIX to match iOS file prefix. * Refactoring package & class names * Package renamed
This commit is contained in:
@@ -38,7 +38,7 @@ android {
|
||||
buildToolsVersion computeBuildToolsVersion()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 17
|
||||
minSdkVersion 16
|
||||
targetSdkVersion computeTargetSdkVersion()
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
@@ -0,0 +1,693 @@
|
||||
/*
|
||||
* Copyright (C) 2008 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 org.nativescript.widgets.image;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* *************************************
|
||||
* Copied from JB release framework:
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java
|
||||
*
|
||||
* so that threading behavior on all OS versions is the same and we can tweak behavior by using
|
||||
* executeOnExecutor() if needed.
|
||||
*
|
||||
* There are 3 changes in this copy of AsyncTask:
|
||||
* -pre-HC a single thread executor is used for serial operation
|
||||
* (Executors.newSingleThreadExecutor) and is the default
|
||||
* -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy
|
||||
* -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added
|
||||
* *************************************
|
||||
*
|
||||
* <p>AsyncTask enables proper and easy use of the UI thread. This class allows to
|
||||
* perform background operations and publish results on the UI thread without
|
||||
* having to manipulate threads and/or handlers.</p>
|
||||
*
|
||||
* <p>AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler}
|
||||
* and does not constitute a generic threading framework. AsyncTasks should ideally be
|
||||
* used for short operations (a few seconds at the most.) If you need to keep threads
|
||||
* running for long periods of time, it is highly recommended you use the various APIs
|
||||
* provided by the <code>java.util.concurrent</code> pacakge such as {@link Executor},
|
||||
* {@link ThreadPoolExecutor} and {@link FutureTask}.</p>
|
||||
*
|
||||
* <p>An asynchronous task is defined by a computation that runs on a background thread and
|
||||
* whose result is published on the UI thread. An asynchronous task is defined by 3 generic
|
||||
* types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>,
|
||||
* and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>,
|
||||
* <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p>
|
||||
*
|
||||
* <div class="special reference">
|
||||
* <h3>Developer Guides</h3>
|
||||
* <p>For more information about using tasks and threads, read the
|
||||
* <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
|
||||
* Threads</a> developer guide.</p>
|
||||
* </div>
|
||||
*
|
||||
* <h2>Usage</h2>
|
||||
* <p>AsyncTask must be subclassed to be used. The subclass will override at least
|
||||
* one method ({@link #doInBackground}), and most often will override a
|
||||
* second one ({@link #onPostExecute}.)</p>
|
||||
*
|
||||
* <p>Here is an example of subclassing:</p>
|
||||
* <pre class="prettyprint">
|
||||
* private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
|
||||
* protected Long doInBackground(URL... urls) {
|
||||
* int count = urls.length;
|
||||
* long totalSize = 0;
|
||||
* for (int i = 0; i < count; i++) {
|
||||
* totalSize += Downloader.downloadFile(urls[i]);
|
||||
* publishProgress((int) ((i / (float) count) * 100));
|
||||
* // Escape early if cancel() is called
|
||||
* if (isCancelled()) break;
|
||||
* }
|
||||
* return totalSize;
|
||||
* }
|
||||
*
|
||||
* protected void onProgressUpdate(Integer... progress) {
|
||||
* setProgressPercent(progress[0]);
|
||||
* }
|
||||
*
|
||||
* protected void onPostExecute(Long result) {
|
||||
* showDialog("Downloaded " + result + " bytes");
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Once created, a task is executed very simply:</p>
|
||||
* <pre class="prettyprint">
|
||||
* new DownloadFilesTask().execute(url1, url2, url3);
|
||||
* </pre>
|
||||
*
|
||||
* <h2>AsyncTask's generic types</h2>
|
||||
* <p>The three types used by an asynchronous task are the following:</p>
|
||||
* <ol>
|
||||
* <li><code>Params</code>, the type of the parameters sent to the task upon
|
||||
* execution.</li>
|
||||
* <li><code>Progress</code>, the type of the progress units published during
|
||||
* the background computation.</li>
|
||||
* <li><code>Result</code>, the type of the result of the background
|
||||
* computation.</li>
|
||||
* </ol>
|
||||
* <p>Not all types are always used by an asynchronous task. To mark a type as unused,
|
||||
* simply use the type {@link Void}:</p>
|
||||
* <pre>
|
||||
* private class MyTask extends AsyncTask<Void, Void, Void> { ... }
|
||||
* </pre>
|
||||
*
|
||||
* <h2>The 4 steps</h2>
|
||||
* <p>When an asynchronous task is executed, the task goes through 4 steps:</p>
|
||||
* <ol>
|
||||
* <li>{@link #onPreExecute()}, invoked on the UI thread immediately after the task
|
||||
* is executed. This step is normally used to setup the task, for instance by
|
||||
* showing a progress bar in the user interface.</li>
|
||||
* <li>{@link #doInBackground}, invoked on the background thread
|
||||
* immediately after {@link #onPreExecute()} finishes executing. This step is used
|
||||
* to perform background computation that can take a long time. The parameters
|
||||
* of the asynchronous task are passed to this step. The result of the computation must
|
||||
* be returned by this step and will be passed back to the last step. This step
|
||||
* can also use {@link #publishProgress} to publish one or more units
|
||||
* of progress. These values are published on the UI thread, in the
|
||||
* {@link #onProgressUpdate} step.</li>
|
||||
* <li>{@link #onProgressUpdate}, invoked on the UI thread after a
|
||||
* call to {@link #publishProgress}. The timing of the execution is
|
||||
* undefined. This method is used to display any form of progress in the user
|
||||
* interface while the background computation is still executing. For instance,
|
||||
* it can be used to animate a progress bar or show logs in a text field.</li>
|
||||
* <li>{@link #onPostExecute}, invoked on the UI thread after the background
|
||||
* computation finishes. The result of the background computation is passed to
|
||||
* this step as a parameter.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Cancelling a task</h2>
|
||||
* <p>A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking
|
||||
* this method will cause subsequent calls to {@link #isCancelled()} to return true.
|
||||
* After invoking this method, {@link #onCancelled(Object)}, instead of
|
||||
* {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])}
|
||||
* returns. To ensure that a task is cancelled as quickly as possible, you should always
|
||||
* check the return value of {@link #isCancelled()} periodically from
|
||||
* {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)</p>
|
||||
*
|
||||
* <h2>Threading rules</h2>
|
||||
* <p>There are a few threading rules that must be followed for this class to
|
||||
* work properly:</p>
|
||||
* <ul>
|
||||
* <li>The AsyncTask class must be loaded on the UI thread. This is done
|
||||
* automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li>
|
||||
* <li>The task instance must be created on the UI thread.</li>
|
||||
* <li>{@link #execute} must be invoked on the UI thread.</li>
|
||||
* <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute},
|
||||
* {@link #doInBackground}, {@link #onProgressUpdate} manually.</li>
|
||||
* <li>The task can be executed only once (an exception will be thrown if
|
||||
* a second execution is attempted.)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Memory observability</h2>
|
||||
* <p>AsyncTask guarantees that all callback calls are synchronized in such a way that the following
|
||||
* operations are safe without explicit synchronizations.</p>
|
||||
* <ul>
|
||||
* <li>Set member fields in the constructor or {@link #onPreExecute}, and refer to them
|
||||
* in {@link #doInBackground}.
|
||||
* <li>Set member fields in {@link #doInBackground}, and refer to them in
|
||||
* {@link #onProgressUpdate} and {@link #onPostExecute}.
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Order of execution</h2>
|
||||
* <p>When first introduced, AsyncTasks were executed serially on a single background
|
||||
* thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
|
||||
* to a pool of threads allowing multiple tasks to operate in parallel. Starting with
|
||||
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single
|
||||
* thread to avoid common application errors caused by parallel execution.</p>
|
||||
* <p>If you truly want parallel execution, you can invoke
|
||||
* {@link #executeOnExecutor(Executor, Object[])} with
|
||||
* {@link #THREAD_POOL_EXECUTOR}.</p>
|
||||
*/
|
||||
public abstract class AsyncTask<Params, Progress, Result> {
|
||||
private static final String LOG_TAG = "AsyncTask";
|
||||
|
||||
private static final int CORE_POOL_SIZE = 5;
|
||||
private static final int MAXIMUM_POOL_SIZE = 128;
|
||||
private static final int KEEP_ALIVE = 1;
|
||||
|
||||
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
|
||||
private final AtomicInteger mCount = new AtomicInteger(1);
|
||||
|
||||
public Thread newThread(Runnable r) {
|
||||
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
|
||||
}
|
||||
};
|
||||
|
||||
private static final BlockingQueue<Runnable> sPoolWorkQueue =
|
||||
new LinkedBlockingQueue<Runnable>(10);
|
||||
|
||||
/**
|
||||
* An {@link Executor} that can be used to execute tasks in parallel.
|
||||
*/
|
||||
public static final Executor THREAD_POOL_EXECUTOR
|
||||
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
|
||||
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory,
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
|
||||
/**
|
||||
* An {@link Executor} that executes tasks one at a time in serial
|
||||
* order. This serialization is global to a particular process.
|
||||
*/
|
||||
public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() :
|
||||
Executors.newSingleThreadExecutor(sThreadFactory);
|
||||
|
||||
public static final Executor DUAL_THREAD_EXECUTOR =
|
||||
Executors.newFixedThreadPool(2, sThreadFactory);
|
||||
|
||||
private static final int MESSAGE_POST_RESULT = 0x1;
|
||||
private static final int MESSAGE_POST_PROGRESS = 0x2;
|
||||
|
||||
private static final InternalHandler sHandler = new InternalHandler();
|
||||
|
||||
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
|
||||
private final WorkerRunnable<Params, Result> mWorker;
|
||||
private final FutureTask<Result> mFuture;
|
||||
|
||||
private volatile Status mStatus = Status.PENDING;
|
||||
|
||||
private final AtomicBoolean mCancelled = new AtomicBoolean();
|
||||
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
|
||||
|
||||
@TargetApi(11)
|
||||
private static class SerialExecutor implements Executor {
|
||||
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
|
||||
Runnable mActive;
|
||||
|
||||
public synchronized void execute(final Runnable r) {
|
||||
mTasks.offer(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
r.run();
|
||||
} finally {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (mActive == null) {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
protected synchronized void scheduleNext() {
|
||||
if ((mActive = mTasks.poll()) != null) {
|
||||
THREAD_POOL_EXECUTOR.execute(mActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the current status of the task. Each status will be set only once
|
||||
* during the lifetime of a task.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* Indicates that the task has not been executed yet.
|
||||
*/
|
||||
PENDING,
|
||||
/**
|
||||
* Indicates that the task is running.
|
||||
*/
|
||||
RUNNING,
|
||||
/**
|
||||
* Indicates that {@link AsyncTask#onPostExecute} has finished.
|
||||
*/
|
||||
FINISHED,
|
||||
}
|
||||
|
||||
/** @hide Used to force static handler to be created. */
|
||||
public static void init() {
|
||||
sHandler.getLooper();
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public static void setDefaultExecutor(Executor exec) {
|
||||
sDefaultExecutor = exec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new asynchronous task. This constructor must be invoked on the UI thread.
|
||||
*/
|
||||
public AsyncTask() {
|
||||
mWorker = new WorkerRunnable<Params, Result>() {
|
||||
public Result call() throws Exception {
|
||||
mTaskInvoked.set(true);
|
||||
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
//noinspection unchecked
|
||||
return postResult(doInBackground(mParams));
|
||||
}
|
||||
};
|
||||
|
||||
mFuture = new FutureTask<Result>(mWorker) {
|
||||
@Override
|
||||
protected void done() {
|
||||
try {
|
||||
postResultIfNotInvoked(get());
|
||||
} catch (InterruptedException e) {
|
||||
android.util.Log.w(LOG_TAG, e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException("An error occured while executing doInBackground()",
|
||||
e.getCause());
|
||||
} catch (CancellationException e) {
|
||||
postResultIfNotInvoked(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void postResultIfNotInvoked(Result result) {
|
||||
final boolean wasTaskInvoked = mTaskInvoked.get();
|
||||
if (!wasTaskInvoked) {
|
||||
postResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
private Result postResult(Result result) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
|
||||
new AsyncTaskResult<Result>(this, result));
|
||||
message.sendToTarget();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current status of this task.
|
||||
*
|
||||
* @return The current status.
|
||||
*/
|
||||
public final Status getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to perform a computation on a background thread. The
|
||||
* specified parameters are the parameters passed to {@link #execute}
|
||||
* by the caller of this task.
|
||||
*
|
||||
* This method can call {@link #publishProgress} to publish updates
|
||||
* on the UI thread.
|
||||
*
|
||||
* @param params The parameters of the task.
|
||||
*
|
||||
* @return A result, defined by the subclass of this task.
|
||||
*
|
||||
* @see #onPreExecute()
|
||||
* @see #onPostExecute
|
||||
* @see #publishProgress
|
||||
*/
|
||||
protected abstract Result doInBackground(Params... params);
|
||||
|
||||
/**
|
||||
* Runs on the UI thread before {@link #doInBackground}.
|
||||
*
|
||||
* @see #onPostExecute
|
||||
* @see #doInBackground
|
||||
*/
|
||||
protected void onPreExecute() {
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Runs on the UI thread after {@link #doInBackground}. The
|
||||
* specified result is the value returned by {@link #doInBackground}.</p>
|
||||
*
|
||||
* <p>This method won't be invoked if the task was cancelled.</p>
|
||||
*
|
||||
* @param result The result of the operation computed by {@link #doInBackground}.
|
||||
*
|
||||
* @see #onPreExecute
|
||||
* @see #doInBackground
|
||||
* @see #onCancelled(Object)
|
||||
*/
|
||||
@SuppressWarnings({"UnusedDeclaration"})
|
||||
protected void onPostExecute(Result result) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on the UI thread after {@link #publishProgress} is invoked.
|
||||
* The specified values are the values passed to {@link #publishProgress}.
|
||||
*
|
||||
* @param values The values indicating progress.
|
||||
*
|
||||
* @see #publishProgress
|
||||
* @see #doInBackground
|
||||
*/
|
||||
@SuppressWarnings({"UnusedDeclaration"})
|
||||
protected void onProgressUpdate(Progress... values) {
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
|
||||
* {@link #doInBackground(Object[])} has finished.</p>
|
||||
*
|
||||
* <p>The default implementation simply invokes {@link #onCancelled()} and
|
||||
* ignores the result. If you write your own implementation, do not call
|
||||
* <code>super.onCancelled(result)</code>.</p>
|
||||
*
|
||||
* @param result The result, if any, computed in
|
||||
* {@link #doInBackground(Object[])}, can be null
|
||||
*
|
||||
* @see #cancel(boolean)
|
||||
* @see #isCancelled()
|
||||
*/
|
||||
@SuppressWarnings({"UnusedParameters"})
|
||||
protected void onCancelled(Result result) {
|
||||
onCancelled();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Applications should preferably override {@link #onCancelled(Object)}.
|
||||
* This method is invoked by the default implementation of
|
||||
* {@link #onCancelled(Object)}.</p>
|
||||
*
|
||||
* <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
|
||||
* {@link #doInBackground(Object[])} has finished.</p>
|
||||
*
|
||||
* @see #onCancelled(Object)
|
||||
* @see #cancel(boolean)
|
||||
* @see #isCancelled()
|
||||
*/
|
||||
protected void onCancelled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if this task was cancelled before it completed
|
||||
* normally. If you are calling {@link #cancel(boolean)} on the task,
|
||||
* the value returned by this method should be checked periodically from
|
||||
* {@link #doInBackground(Object[])} to end the task as soon as possible.
|
||||
*
|
||||
* @return <tt>true</tt> if task was cancelled before it completed
|
||||
*
|
||||
* @see #cancel(boolean)
|
||||
*/
|
||||
public final boolean isCancelled() {
|
||||
return mCancelled.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Attempts to cancel execution of this task. This attempt will
|
||||
* fail if the task has already completed, already been cancelled,
|
||||
* or could not be cancelled for some other reason. If successful,
|
||||
* and this task has not started when <tt>cancel</tt> is called,
|
||||
* this task should never run. If the task has already started,
|
||||
* then the <tt>mayInterruptIfRunning</tt> parameter determines
|
||||
* whether the thread executing this task should be interrupted in
|
||||
* an attempt to stop the task.</p>
|
||||
*
|
||||
* <p>Calling this method will result in {@link #onCancelled(Object)} being
|
||||
* invoked on the UI thread after {@link #doInBackground(Object[])}
|
||||
* returns. Calling this method guarantees that {@link #onPostExecute(Object)}
|
||||
* is never invoked. After invoking this method, you should check the
|
||||
* value returned by {@link #isCancelled()} periodically from
|
||||
* {@link #doInBackground(Object[])} to finish the task as early as
|
||||
* possible.</p>
|
||||
*
|
||||
* @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
|
||||
* task should be interrupted; otherwise, in-progress tasks are allowed
|
||||
* to complete.
|
||||
*
|
||||
* @return <tt>false</tt> if the task could not be cancelled,
|
||||
* typically because it has already completed normally;
|
||||
* <tt>true</tt> otherwise
|
||||
*
|
||||
* @see #isCancelled()
|
||||
* @see #onCancelled(Object)
|
||||
*/
|
||||
public final boolean cancel(boolean mayInterruptIfRunning) {
|
||||
mCancelled.set(true);
|
||||
return mFuture.cancel(mayInterruptIfRunning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits if necessary for the computation to complete, and then
|
||||
* retrieves its result.
|
||||
*
|
||||
* @return The computed result.
|
||||
*
|
||||
* @throws CancellationException If the computation was cancelled.
|
||||
* @throws ExecutionException If the computation threw an exception.
|
||||
* @throws InterruptedException If the current thread was interrupted
|
||||
* while waiting.
|
||||
*/
|
||||
public final Result get() throws InterruptedException, ExecutionException {
|
||||
return mFuture.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits if necessary for at most the given time for the computation
|
||||
* to complete, and then retrieves its result.
|
||||
*
|
||||
* @param timeout Time to wait before cancelling the operation.
|
||||
* @param unit The time unit for the timeout.
|
||||
*
|
||||
* @return The computed result.
|
||||
*
|
||||
* @throws CancellationException If the computation was cancelled.
|
||||
* @throws ExecutionException If the computation threw an exception.
|
||||
* @throws InterruptedException If the current thread was interrupted
|
||||
* while waiting.
|
||||
* @throws TimeoutException If the wait timed out.
|
||||
*/
|
||||
public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
|
||||
ExecutionException, TimeoutException {
|
||||
return mFuture.get(timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the task with the specified parameters. The task returns
|
||||
* itself (this) so that the caller can keep a reference to it.
|
||||
*
|
||||
* <p>Note: this function schedules the task on a queue for a single background
|
||||
* thread or pool of threads depending on the platform version. When first
|
||||
* introduced, AsyncTasks were executed serially on a single background thread.
|
||||
* Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
|
||||
* to a pool of threads allowing multiple tasks to operate in parallel. Starting
|
||||
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
|
||||
* executed on a single thread to avoid common application errors caused
|
||||
* by parallel execution. If you truly want parallel execution, you can use
|
||||
* the {@link #executeOnExecutor} version of this method
|
||||
* with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
|
||||
* on its use.
|
||||
*
|
||||
* <p>This method must be invoked on the UI thread.
|
||||
*
|
||||
* @param params The parameters of the task.
|
||||
*
|
||||
* @return This instance of AsyncTask.
|
||||
*
|
||||
* @throws IllegalStateException If {@link #getStatus()} returns either
|
||||
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
|
||||
*
|
||||
* @see #executeOnExecutor(Executor, Object[])
|
||||
* @see #execute(Runnable)
|
||||
*/
|
||||
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
|
||||
return executeOnExecutor(sDefaultExecutor, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the task with the specified parameters. The task returns
|
||||
* itself (this) so that the caller can keep a reference to it.
|
||||
*
|
||||
* <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to
|
||||
* allow multiple tasks to run in parallel on a pool of threads managed by
|
||||
* AsyncTask, however you can also use your own {@link Executor} for custom
|
||||
* behavior.
|
||||
*
|
||||
* <p><em>Warning:</em> Allowing multiple tasks to run in parallel from
|
||||
* a thread pool is generally <em>not</em> what one wants, because the order
|
||||
* of their operation is not defined. For example, if these tasks are used
|
||||
* to modify any state in common (such as writing a file due to a button click),
|
||||
* there are no guarantees on the order of the modifications.
|
||||
* Without careful work it is possible in rare cases for the newer version
|
||||
* of the data to be over-written by an older one, leading to obscure data
|
||||
* loss and stability issues. Such changes are best
|
||||
* executed in serial; to guarantee such work is serialized regardless of
|
||||
* platform version you can use this function with {@link #SERIAL_EXECUTOR}.
|
||||
*
|
||||
* <p>This method must be invoked on the UI thread.
|
||||
*
|
||||
* @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a
|
||||
* convenient process-wide thread pool for tasks that are loosely coupled.
|
||||
* @param params The parameters of the task.
|
||||
*
|
||||
* @return This instance of AsyncTask.
|
||||
*
|
||||
* @throws IllegalStateException If {@link #getStatus()} returns either
|
||||
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
|
||||
*
|
||||
* @see #execute(Object[])
|
||||
*/
|
||||
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
|
||||
Params... params) {
|
||||
if (mStatus != Status.PENDING) {
|
||||
switch (mStatus) {
|
||||
case RUNNING:
|
||||
throw new IllegalStateException("Cannot execute task:"
|
||||
+ " the task is already running.");
|
||||
case FINISHED:
|
||||
throw new IllegalStateException("Cannot execute task:"
|
||||
+ " the task has already been executed "
|
||||
+ "(a task can be executed only once)");
|
||||
}
|
||||
}
|
||||
|
||||
mStatus = Status.RUNNING;
|
||||
|
||||
onPreExecute();
|
||||
|
||||
mWorker.mParams = params;
|
||||
exec.execute(mFuture);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience version of {@link #execute(Object...)} for use with
|
||||
* a simple Runnable object. See {@link #execute(Object[])} for more
|
||||
* information on the order of execution.
|
||||
*
|
||||
* @see #execute(Object[])
|
||||
* @see #executeOnExecutor(Executor, Object[])
|
||||
*/
|
||||
public static void execute(Runnable runnable) {
|
||||
sDefaultExecutor.execute(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method can be invoked from {@link #doInBackground} to
|
||||
* publish updates on the UI thread while the background computation is
|
||||
* still running. Each call to this method will trigger the execution of
|
||||
* {@link #onProgressUpdate} on the UI thread.
|
||||
*
|
||||
* {@link #onProgressUpdate} will note be called if the task has been
|
||||
* canceled.
|
||||
*
|
||||
* @param values The progress values to update the UI with.
|
||||
*
|
||||
* @see #onProgressUpdate
|
||||
* @see #doInBackground
|
||||
*/
|
||||
protected final void publishProgress(Progress... values) {
|
||||
if (!isCancelled()) {
|
||||
sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
|
||||
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
private void finish(Result result) {
|
||||
if (isCancelled()) {
|
||||
onCancelled(result);
|
||||
} else {
|
||||
onPostExecute(result);
|
||||
}
|
||||
mStatus = Status.FINISHED;
|
||||
}
|
||||
|
||||
private static class InternalHandler extends Handler {
|
||||
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
AsyncTaskResult result = (AsyncTaskResult) msg.obj;
|
||||
switch (msg.what) {
|
||||
case MESSAGE_POST_RESULT:
|
||||
// There is only one result
|
||||
result.mTask.finish(result.mData[0]);
|
||||
break;
|
||||
case MESSAGE_POST_PROGRESS:
|
||||
result.mTask.onProgressUpdate(result.mData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
|
||||
Params[] mParams;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"RawUseOfParameterizedType"})
|
||||
private static class AsyncTaskResult<Data> {
|
||||
final AsyncTask mTask;
|
||||
final Data[] mData;
|
||||
|
||||
AsyncTaskResult(AsyncTask task, Data... data) {
|
||||
mTask = task;
|
||||
mData = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
/*
|
||||
* 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 org.nativescript.widgets.image;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Environment;
|
||||
import android.os.StatFs;
|
||||
import android.support.v4.util.LruCache;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class handles disk and memory caching of bitmaps in conjunction with the
|
||||
* {@link Worker} class and its subclasses. Use
|
||||
* {@link Cache#getInstance(CacheParams)} to get an instance of this
|
||||
* class, although usually a cache should be added directly to an {@link Worker} by calling
|
||||
* {@link Worker#addImageCache(Cache)}.
|
||||
*/
|
||||
public class Cache {
|
||||
private static final String TAG = "JS";
|
||||
|
||||
// Default memory cache size in kilobytes
|
||||
private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB
|
||||
|
||||
// Default disk cache size in bytes
|
||||
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
|
||||
|
||||
// Compression settings when writing images to disk cache
|
||||
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
|
||||
private static final int DEFAULT_COMPRESS_QUALITY = 70;
|
||||
private static final int DISK_CACHE_INDEX = 0;
|
||||
|
||||
// Constants to easily toggle various caches
|
||||
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
|
||||
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
|
||||
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
|
||||
|
||||
private static final String IMAGE_CACHE_DIR = "images";
|
||||
|
||||
private static Cache instance;
|
||||
private DiskLruCache mDiskLruCache;
|
||||
private LruCache<String, BitmapDrawable> mMemoryCache;
|
||||
private CacheParams mParams;
|
||||
private final Object mDiskCacheLock = new Object();
|
||||
private boolean mDiskCacheStarting = true;
|
||||
|
||||
private Set<SoftReference<Bitmap>> mReusableBitmaps;
|
||||
|
||||
private Fetcher mFetcher;
|
||||
|
||||
/**
|
||||
* Create a new Cache object using the specified parameters. This should not be
|
||||
* called directly by other classes, instead use
|
||||
* {@link Cache#getInstance(CacheParams)} to fetch an Cache
|
||||
* instance.
|
||||
*
|
||||
* @param cacheParams The cache parameters to use to initialize the cache
|
||||
*/
|
||||
private Cache(CacheParams cacheParams) {
|
||||
init(cacheParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link Cache} instance.
|
||||
*
|
||||
* @return An existing retained Cache object or a new one if one did not exist
|
||||
*/
|
||||
public static Cache getInstance(CacheParams cacheParams) {
|
||||
if (instance == null) {
|
||||
instance = new Cache(cacheParams);
|
||||
instance.init(cacheParams);
|
||||
}
|
||||
else if (instance.mParams != cacheParams) {
|
||||
instance.init(cacheParams);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the cache, providing all parameters.
|
||||
*
|
||||
* @param cacheParams The cache parameters to initialize the cache
|
||||
*/
|
||||
private void init(CacheParams cacheParams) {
|
||||
mParams = cacheParams;
|
||||
|
||||
//BEGIN_INCLUDE(init_memory_cache)
|
||||
// Set up memory cache
|
||||
if (mParams.memoryCacheEnabled) {
|
||||
if (Worker.debuggable > 0) {
|
||||
Log.v(TAG, "Memory cache created (size = " + mParams.memCacheSize + ")");
|
||||
}
|
||||
|
||||
// If we're running on Honeycomb or newer, create a set of reusable bitmaps that can be
|
||||
// populated into the inBitmap field of BitmapFactory.Options. Note that the set is
|
||||
// of SoftReferences which will actually not be very effective due to the garbage
|
||||
// collector being aggressive clearing Soft/WeakReferences. A better approach
|
||||
// would be to use a strongly references bitmaps, however this would require some
|
||||
// balancing of memory usage between this set and the bitmap LruCache. It would also
|
||||
// require knowledge of the expected size of the bitmaps. From Honeycomb to JellyBean
|
||||
// the size would need to be precise, from KitKat onward the size would just need to
|
||||
// be the upper bound (due to changes in how inBitmap can re-use bitmaps).
|
||||
if (Utils.hasHoneycomb()) {
|
||||
mReusableBitmaps =
|
||||
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
|
||||
}
|
||||
|
||||
mMemoryCache = new LruCache<String, BitmapDrawable>(mParams.memCacheSize) {
|
||||
|
||||
/**
|
||||
* Notify the removed entry that is no longer being cached
|
||||
*/
|
||||
@Override
|
||||
protected void entryRemoved(boolean evicted, String key,
|
||||
BitmapDrawable oldValue, BitmapDrawable newValue) {
|
||||
if (Utils.hasHoneycomb()) {
|
||||
// We're running on Honeycomb or later, so add the bitmap
|
||||
// to a SoftReference set for possible use with inBitmap later
|
||||
mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure item size in kilobytes rather than units which is more practical
|
||||
* for a bitmap cache
|
||||
*/
|
||||
@Override
|
||||
protected int sizeOf(String key, BitmapDrawable value) {
|
||||
final int bitmapSize = getBitmapSize(value) / 1024;
|
||||
return bitmapSize == 0 ? 1 : bitmapSize;
|
||||
}
|
||||
};
|
||||
}
|
||||
//END_INCLUDE(init_memory_cache)
|
||||
|
||||
// // By default the disk cache is not initialized here as it should be initialized
|
||||
// // on a separate thread due to disk access.
|
||||
// if (cacheParams.initDiskCacheOnCreate) {
|
||||
// // Set up disk cache
|
||||
// initDiskCache();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the disk cache. Note that this includes disk access so this should not be
|
||||
* executed on the main/UI thread. By default an Cache does not initialize the disk
|
||||
* cache when it is created, instead you should call initDiskCache() to initialize it on a
|
||||
* background thread.
|
||||
*/
|
||||
public void initDiskCache() {
|
||||
// Set up disk cache
|
||||
synchronized (mDiskCacheLock) {
|
||||
if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
|
||||
File diskCacheDir = mParams.diskCacheDir;
|
||||
if (mParams.diskCacheEnabled && diskCacheDir != null) {
|
||||
if (!diskCacheDir.exists()) {
|
||||
diskCacheDir.mkdirs();
|
||||
}
|
||||
if (getUsableSpace(diskCacheDir) > mParams.diskCacheSize) {
|
||||
try {
|
||||
mDiskLruCache = DiskLruCache.open(
|
||||
diskCacheDir, 1, 1, mParams.diskCacheSize);
|
||||
if (Worker.debuggable > 0) {
|
||||
Log.v(TAG, "Disk cache initialized");
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
mParams.diskCacheDir = null;
|
||||
Log.e(TAG, "initDiskCache - " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mDiskCacheStarting = false;
|
||||
mDiskCacheLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a bitmap to both memory and disk cache.
|
||||
*
|
||||
* @param data Unique identifier for the bitmap to store
|
||||
* @param value The bitmap drawable to store
|
||||
*/
|
||||
public void addBitmapToCache(String data, BitmapDrawable value, boolean useDiskCache) {
|
||||
//BEGIN_INCLUDE(add_bitmap_to_cache)
|
||||
if (data == null || value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to memory cache
|
||||
if (mMemoryCache != null) {
|
||||
mMemoryCache.put(data, value);
|
||||
}
|
||||
|
||||
if (!useDiskCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (mDiskCacheLock) {
|
||||
// Add to disk cache
|
||||
if (mDiskLruCache != null) {
|
||||
final String key = hashKeyForDisk(data);
|
||||
OutputStream out = null;
|
||||
try {
|
||||
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
|
||||
if (snapshot == null) {
|
||||
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
|
||||
if (editor != null) {
|
||||
out = editor.newOutputStream(DISK_CACHE_INDEX);
|
||||
value.getBitmap().compress(
|
||||
mParams.compressFormat, mParams.compressQuality, out);
|
||||
editor.commit();
|
||||
out.close();
|
||||
}
|
||||
} else {
|
||||
snapshot.getInputStream(DISK_CACHE_INDEX).close();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "addBitmapToCache - " + e);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "addBitmapToCache - " + e);
|
||||
} finally {
|
||||
try {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//END_INCLUDE(add_bitmap_to_cache)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get from memory cache.
|
||||
*
|
||||
* @param data Unique identifier for which item to get
|
||||
* @return The bitmap drawable if found in cache, null otherwise
|
||||
*/
|
||||
public BitmapDrawable getBitmapFromMemCache(String data) {
|
||||
//BEGIN_INCLUDE(get_bitmap_from_mem_cache)
|
||||
BitmapDrawable memValue = null;
|
||||
|
||||
if (mMemoryCache != null) {
|
||||
memValue = mMemoryCache.get(data);
|
||||
}
|
||||
|
||||
if (Worker.debuggable > 0 && memValue != null) {
|
||||
Log.v(TAG, "Memory cache hit");
|
||||
}
|
||||
|
||||
return memValue;
|
||||
//END_INCLUDE(get_bitmap_from_mem_cache)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get from disk cache.
|
||||
*
|
||||
* @param data Unique identifier for which item to get
|
||||
* @return The bitmap if found in cache, null otherwise
|
||||
*/
|
||||
public Bitmap getBitmapFromDiskCache(String data) {
|
||||
//BEGIN_INCLUDE(get_bitmap_from_disk_cache)
|
||||
final String key = hashKeyForDisk(data);
|
||||
Bitmap bitmap = null;
|
||||
|
||||
synchronized (mDiskCacheLock) {
|
||||
while (mDiskCacheStarting) {
|
||||
try {
|
||||
mDiskCacheLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
if (mDiskLruCache != null) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
|
||||
if (snapshot != null) {
|
||||
if (Worker.debuggable > 0) {
|
||||
Log.v(TAG, "Disk cache hit");
|
||||
}
|
||||
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
|
||||
if (inputStream != null) {
|
||||
FileDescriptor fd = ((FileInputStream) inputStream).getFD();
|
||||
|
||||
// Decode bitmap, but we don't want to sample so give
|
||||
// MAX_VALUE as the target dimensions
|
||||
bitmap = Resizer.decodeSampledBitmapFromDescriptor(
|
||||
fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this);
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "getBitmapFromDiskCache - " + e);
|
||||
} finally {
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
//END_INCLUDE(get_bitmap_from_disk_cache)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param options - BitmapFactory.Options with out* options populated
|
||||
* @return Bitmap that case be used for inBitmap
|
||||
*/
|
||||
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
|
||||
//BEGIN_INCLUDE(get_bitmap_from_reusable_set)
|
||||
Bitmap bitmap = null;
|
||||
|
||||
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
|
||||
synchronized (mReusableBitmaps) {
|
||||
final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
|
||||
Bitmap item;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
item = iterator.next().get();
|
||||
|
||||
if (null != item && item.isMutable()) {
|
||||
// Check to see it the item can be used for inBitmap
|
||||
if (canUseForInBitmap(item, options)) {
|
||||
bitmap = item;
|
||||
|
||||
// Remove from reusable set so it can't be used again
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Remove from the set if the reference has been cleared.
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
//END_INCLUDE(get_bitmap_from_reusable_set)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears both the memory and disk cache associated with this Cache object. Note that
|
||||
* this includes disk access so this should not be executed on the main/UI thread.
|
||||
*/
|
||||
public void clearCache() {
|
||||
if (mMemoryCache != null) {
|
||||
mMemoryCache.evictAll();
|
||||
if (Worker.debuggable > 0) {
|
||||
Log.v(TAG, "Memory cache cleared");
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (mDiskCacheLock) {
|
||||
mDiskCacheStarting = true;
|
||||
if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
|
||||
try {
|
||||
mDiskLruCache.delete();
|
||||
if (Worker.debuggable > 0) {
|
||||
Log.v(TAG, "Disk cache cleared");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "clearCache - " + e);
|
||||
}
|
||||
mDiskLruCache = null;
|
||||
// initDiskCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the disk cache associated with this Cache object. Note that this includes
|
||||
* disk access so this should not be executed on the main/UI thread.
|
||||
*/
|
||||
public void flush() {
|
||||
synchronized (mDiskCacheLock) {
|
||||
if (mDiskLruCache != null) {
|
||||
try {
|
||||
mDiskLruCache.flush();
|
||||
if (Worker.debuggable > 0) {
|
||||
Log.v(TAG, "Disk cache flushed");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "flush - " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the disk cache associated with this Cache object. Note that this includes
|
||||
* disk access so this should not be executed on the main/UI thread.
|
||||
*/
|
||||
public void close() {
|
||||
synchronized (mDiskCacheLock) {
|
||||
if (mDiskLruCache != null) {
|
||||
try {
|
||||
if (!mDiskLruCache.isClosed()) {
|
||||
mDiskLruCache.close();
|
||||
mDiskLruCache = null;
|
||||
if (Worker.debuggable > 0) {
|
||||
Log.v(TAG, "Disk cache closed");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "close - " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A holder class that contains cache parameters.
|
||||
*/
|
||||
public static class CacheParams {
|
||||
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
|
||||
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
|
||||
public File diskCacheDir;
|
||||
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
|
||||
public int compressQuality = DEFAULT_COMPRESS_QUALITY;
|
||||
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
|
||||
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
|
||||
|
||||
/**
|
||||
* Create a set of image cache parameters that can be provided to
|
||||
* {@link Cache#getInstance(CacheParams)}.
|
||||
*
|
||||
* @param context A context to use.
|
||||
* @param diskCacheDirectoryName A unique subdirectory name that will be appended to the
|
||||
* application cache directory. Usually "cache" or "images"
|
||||
* is sufficient.
|
||||
*/
|
||||
public CacheParams(Context context, String diskCacheDirectoryName) {
|
||||
diskCacheDir = getDiskCacheDir(context, diskCacheDirectoryName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the memory cache size based on a percentage of the max available VM memory.
|
||||
* Eg. setting percent to 0.2 would set the memory cache to one fifth of the available
|
||||
* memory. Throws {@link IllegalArgumentException} if percent is < 0.01 or > .8.
|
||||
* memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed
|
||||
* to construct a LruCache which takes an int in its constructor.
|
||||
* <p>
|
||||
* This value should be chosen carefully based on a number of factors
|
||||
* Refer to the corresponding Android Training class for more discussion:
|
||||
* http://developer.android.com/training/displaying-bitmaps/
|
||||
*
|
||||
* @param percent Percent of available app memory to use to size memory cache
|
||||
*/
|
||||
public void setMemCacheSizePercent(float percent) {
|
||||
if (percent < 0.01f || percent > 0.8f) {
|
||||
throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
|
||||
+ "between 0.01 and 0.8 (inclusive)");
|
||||
}
|
||||
memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param candidate - Bitmap to check
|
||||
* @param targetOptions - Options that have the out* value populated
|
||||
* @return true if <code>candidate</code> can be used for inBitmap re-use with
|
||||
* <code>targetOptions</code>
|
||||
*/
|
||||
@TargetApi(VERSION_CODES.KITKAT)
|
||||
private static boolean canUseForInBitmap(
|
||||
Bitmap candidate, BitmapFactory.Options targetOptions) {
|
||||
//BEGIN_INCLUDE(can_use_for_inbitmap)
|
||||
if (!Utils.hasKitKat()) {
|
||||
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
|
||||
return candidate.getWidth() == targetOptions.outWidth
|
||||
&& candidate.getHeight() == targetOptions.outHeight
|
||||
&& targetOptions.inSampleSize == 1;
|
||||
}
|
||||
|
||||
// From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap
|
||||
// is smaller than the reusable bitmap candidate allocation byte count.
|
||||
int width = targetOptions.outWidth / targetOptions.inSampleSize;
|
||||
int height = targetOptions.outHeight / targetOptions.inSampleSize;
|
||||
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
|
||||
return byteCount <= candidate.getAllocationByteCount();
|
||||
//END_INCLUDE(can_use_for_inbitmap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the byte usage per pixel of a bitmap based on its configuration.
|
||||
*
|
||||
* @param config The bitmap configuration.
|
||||
* @return The byte usage per pixel.
|
||||
*/
|
||||
private static int getBytesPerPixel(Config config) {
|
||||
if (config == Config.ARGB_8888) {
|
||||
return 4;
|
||||
} else if (config == Config.RGB_565) {
|
||||
return 2;
|
||||
} else if (config == Config.ARGB_4444) {
|
||||
return 2;
|
||||
} else if (config == Config.ALPHA_8) {
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a usable cache directory (external if available, internal otherwise).
|
||||
*
|
||||
* @param context The context to use
|
||||
* @param uniqueName A unique directory name to append to the cache dir
|
||||
* @return The cache dir
|
||||
*/
|
||||
public static File getDiskCacheDir(Context context, String uniqueName) {
|
||||
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
|
||||
// otherwise use internal cache dir
|
||||
final String cachePath =
|
||||
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
|
||||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
|
||||
context.getCacheDir().getPath();
|
||||
|
||||
return new File(cachePath + File.separator + uniqueName);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashing method that changes a string (like a URL) into a hash suitable for using as a
|
||||
* disk filename.
|
||||
*/
|
||||
public static String hashKeyForDisk(String key) {
|
||||
String cacheKey;
|
||||
try {
|
||||
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
|
||||
mDigest.update(key.getBytes());
|
||||
cacheKey = bytesToHexString(mDigest.digest());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
private static String bytesToHexString(byte[] bytes) {
|
||||
// http://stackoverflow.com/questions/332079
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
String hex = Integer.toHexString(0xFF & bytes[i]);
|
||||
if (hex.length() == 1) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(hex);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size in bytes of a bitmap in a BitmapDrawable. Note that from Android 4.4 (KitKat)
|
||||
* onward this returns the allocated memory size of the bitmap which can be larger than the
|
||||
* actual bitmap data byte count (in the case it was re-used).
|
||||
*
|
||||
* @param value
|
||||
* @return size in bytes
|
||||
*/
|
||||
@TargetApi(VERSION_CODES.KITKAT)
|
||||
public static int getBitmapSize(BitmapDrawable value) {
|
||||
Bitmap bitmap = value.getBitmap();
|
||||
|
||||
// From KitKat onward use getAllocationByteCount() as allocated bytes can potentially be
|
||||
// larger than bitmap byte count.
|
||||
if (Utils.hasKitKat()) {
|
||||
return bitmap.getAllocationByteCount();
|
||||
}
|
||||
|
||||
if (Utils.hasHoneycombMR1()) {
|
||||
return bitmap.getByteCount();
|
||||
}
|
||||
|
||||
// Pre HC-MR1
|
||||
return bitmap.getRowBytes() * bitmap.getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if external storage is built-in or removable.
|
||||
*
|
||||
* @return True if external storage is removable (like an SD card), false
|
||||
* otherwise.
|
||||
*/
|
||||
@TargetApi(VERSION_CODES.GINGERBREAD)
|
||||
public static boolean isExternalStorageRemovable() {
|
||||
if (Utils.hasGingerbread()) {
|
||||
return Environment.isExternalStorageRemovable();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the external app cache directory.
|
||||
*
|
||||
* @param context The context to use
|
||||
* @return The external cache dir
|
||||
*/
|
||||
@TargetApi(VERSION_CODES.FROYO)
|
||||
public static File getExternalCacheDir(Context context) {
|
||||
if (Utils.hasFroyo()) {
|
||||
return context.getExternalCacheDir();
|
||||
}
|
||||
|
||||
// Before Froyo we need to construct the external cache dir ourselves
|
||||
final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
|
||||
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check how much usable space is available at a given path.
|
||||
*
|
||||
* @param path The path to check
|
||||
* @return The space available in bytes
|
||||
*/
|
||||
@TargetApi(VERSION_CODES.GINGERBREAD)
|
||||
public static long getUsableSpace(File path) {
|
||||
if (Utils.hasGingerbread()) {
|
||||
return path.getUsableSpace();
|
||||
}
|
||||
final StatFs stats = new StatFs(path.getPath());
|
||||
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
/*
|
||||
* 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 org.nativescript.widgets.image;
|
||||
|
||||
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(T[] original, int start, 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(Reader reader) throws IOException {
|
||||
try {
|
||||
StringWriter writer = new StringWriter();
|
||||
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 EOFException if the stream is exhausted before the next newline
|
||||
* character.
|
||||
*/
|
||||
public static String readAsciiLine(InputStream in) throws IOException {
|
||||
// TODO: support UTF-8 here instead
|
||||
|
||||
StringBuilder result = new StringBuilder(80);
|
||||
while (true) {
|
||||
int c = in.read();
|
||||
if (c == -1) {
|
||||
throw new EOFException();
|
||||
} else if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
|
||||
result.append((char) c);
|
||||
}
|
||||
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(Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete everything in {@code dir}.
|
||||
*/
|
||||
// TODO: this should specify paths as Strings rather than as Files
|
||||
public static void deleteContents(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IllegalArgumentException("not a directory: " + dir);
|
||||
}
|
||||
for (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(File directory, int appVersion, int valueCount, long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||
this.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(File directory, int appVersion, int valueCount, 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 (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 {
|
||||
InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
|
||||
try {
|
||||
String magic = readAsciiLine(in);
|
||||
String version = readAsciiLine(in);
|
||||
String appVersionString = readAsciiLine(in);
|
||||
String valueCountString = readAsciiLine(in);
|
||||
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 (EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
closeQuietly(in);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(String line) throws IOException {
|
||||
String[] parts = line.split(" ");
|
||||
if (parts.length < 2) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
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 (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||
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();
|
||||
}
|
||||
|
||||
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 (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(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(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
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.
|
||||
*/
|
||||
InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (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(String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(String key, 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
|
||||
}
|
||||
|
||||
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(Editor editor, boolean success) throws IOException {
|
||||
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++) {
|
||||
File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
long oldLength = entry.lengths[i];
|
||||
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(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
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.
|
||||
*/
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // already closed
|
||||
}
|
||||
for (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(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(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(String key, long sequenceNumber, 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(int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for {@code index}.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
for (InputStream in : ins) {
|
||||
closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the values for an entry.
|
||||
*/
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
private boolean hasErrors;
|
||||
|
||||
private Editor(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(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(int index) throws IOException {
|
||||
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(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(int index, 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(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override public void write(int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void write(byte[] buffer, int offset, int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (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(String key) {
|
||||
this.key = key;
|
||||
this.lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() throws IOException {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lengths using decimal numbers like "10123".
|
||||
*/
|
||||
private void setLengths(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 (NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* 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 org.nativescript.widgets.image;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* A simple subclass of {@link Resizer} that fetch and resize images from a file, resource or URL.
|
||||
*/
|
||||
public class Fetcher extends Resizer {
|
||||
private static final String RESOURCE_PREFIX = "res://";
|
||||
private static final String FILE_PREFIX = "file:///";
|
||||
private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
private static final String HTTP_CACHE_DIR = "http";
|
||||
private static final int IO_BUFFER_SIZE = 8 * 1024;
|
||||
|
||||
private DiskLruCache mHttpDiskCache;
|
||||
private File mHttpCacheDir;
|
||||
private boolean mHttpDiskCacheStarting = true;
|
||||
private final Object mHttpDiskCacheLock = new Object();
|
||||
private static final int DISK_CACHE_INDEX = 0;
|
||||
|
||||
private final String mPackageName;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize providing a target image width and height for the processing images.
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
public Fetcher(Context context) {
|
||||
super(context);
|
||||
checkConnection(context);
|
||||
mHttpCacheDir = Cache.getDiskCacheDir(context, HTTP_CACHE_DIR);
|
||||
mPackageName = context.getPackageName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDiskCacheInternal() {
|
||||
super.initDiskCacheInternal();
|
||||
initHttpDiskCache();
|
||||
}
|
||||
|
||||
private void initHttpDiskCache() {
|
||||
if (!mHttpCacheDir.exists()) {
|
||||
mHttpCacheDir.mkdirs();
|
||||
}
|
||||
synchronized (mHttpDiskCacheLock) {
|
||||
if (Cache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) {
|
||||
try {
|
||||
mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE);
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "HTTP cache initialized");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
mHttpDiskCache = null;
|
||||
}
|
||||
}
|
||||
mHttpDiskCacheStarting = false;
|
||||
mHttpDiskCacheLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void clearCacheInternal() {
|
||||
super.clearCacheInternal();
|
||||
synchronized (mHttpDiskCacheLock) {
|
||||
if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) {
|
||||
try {
|
||||
mHttpDiskCache.delete();
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "HTTP cache cleared");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "clearCacheInternal - " + e);
|
||||
}
|
||||
mHttpDiskCache = null;
|
||||
mHttpDiskCacheStarting = true;
|
||||
// initHttpDiskCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void flushCacheInternal() {
|
||||
super.flushCacheInternal();
|
||||
synchronized (mHttpDiskCacheLock) {
|
||||
if (mHttpDiskCache != null) {
|
||||
try {
|
||||
mHttpDiskCache.flush();
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "HTTP cache flushed");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "flush - " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeCacheInternal() {
|
||||
super.closeCacheInternal();
|
||||
synchronized (mHttpDiskCacheLock) {
|
||||
if (mHttpDiskCache != null) {
|
||||
try {
|
||||
if (!mHttpDiskCache.isClosed()) {
|
||||
mHttpDiskCache.close();
|
||||
mHttpDiskCache = null;
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "HTTP cache closed");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "closeCacheInternal - " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple network connection check.
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
private void checkConnection(Context context) {
|
||||
final ConnectivityManager cm =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
|
||||
if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
|
||||
Log.e(TAG, "checkConnection - no connection found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main process method, which will be called by the Worker in the AsyncTask background
|
||||
* thread.
|
||||
*
|
||||
* @param data The data to load the bitmap, in this case, a regular http URL
|
||||
* @return The downloaded and resized bitmap
|
||||
*/
|
||||
private Bitmap processBitmap(String data, int decodeWidth, int decodeHeight) {
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "processBitmap - " + data);
|
||||
}
|
||||
|
||||
final String key = Cache.hashKeyForDisk(data);
|
||||
FileDescriptor fileDescriptor = null;
|
||||
FileInputStream fileInputStream = null;
|
||||
DiskLruCache.Snapshot snapshot;
|
||||
synchronized (mHttpDiskCacheLock) {
|
||||
// Wait for disk cache to initialize
|
||||
while (mHttpDiskCacheStarting) {
|
||||
try {
|
||||
mHttpDiskCacheLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (mHttpDiskCache != null) {
|
||||
try {
|
||||
snapshot = mHttpDiskCache.get(key);
|
||||
if (snapshot == null) {
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "processBitmap, not found in http cache, downloading...");
|
||||
}
|
||||
DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
|
||||
if (editor != null) {
|
||||
if (downloadUrlToStream(data,
|
||||
editor.newOutputStream(DISK_CACHE_INDEX))) {
|
||||
editor.commit();
|
||||
} else {
|
||||
editor.abort();
|
||||
}
|
||||
}
|
||||
snapshot = mHttpDiskCache.get(key);
|
||||
}
|
||||
if (snapshot != null) {
|
||||
fileInputStream =
|
||||
(FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
|
||||
fileDescriptor = fileInputStream.getFD();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "processBitmap - " + e);
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(TAG, "processBitmap - " + e);
|
||||
} finally {
|
||||
if (fileDescriptor == null && fileInputStream != null) {
|
||||
try {
|
||||
fileInputStream.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap bitmap = null;
|
||||
if (fileDescriptor != null) {
|
||||
bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, decodeWidth,
|
||||
decodeHeight, getImageCache());
|
||||
}
|
||||
if (fileInputStream != null) {
|
||||
try {
|
||||
fileInputStream.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap processBitmap(Object data, int decodeWidth, int decodeHeight) {
|
||||
if (data instanceof String) {
|
||||
String stringData = String.valueOf(data);
|
||||
if (stringData.startsWith(FILE_PREFIX)) {
|
||||
String filename = stringData.substring(FILE_PREFIX.length());
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "processBitmap - " + filename);
|
||||
}
|
||||
return decodeSampledBitmapFromFile(filename, decodeWidth,
|
||||
decodeHeight, getImageCache());
|
||||
} else if (stringData.startsWith(RESOURCE_PREFIX)) {
|
||||
String resPath = stringData.substring(RESOURCE_PREFIX.length());
|
||||
int resId = mResources.getIdentifier(stringData, "drawable", mPackageName);
|
||||
if (resId > 0) {
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "processBitmap - " + resId);
|
||||
}
|
||||
return decodeSampledBitmapFromResource(mResources, resId, decodeWidth,
|
||||
decodeHeight, getImageCache());
|
||||
} else {
|
||||
Log.v(TAG, "Missing ResourceID: " + stringData);
|
||||
}
|
||||
} else {
|
||||
return processBitmap(stringData, decodeWidth, decodeHeight);
|
||||
}
|
||||
} else {
|
||||
Log.v(TAG, "Invalid Value: " + String.valueOf(data) + ". Expecting String or Integer.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a bitmap from a URL and write the content to an output stream.
|
||||
*
|
||||
* @param urlString The URL to fetch
|
||||
* @return true if successful, false otherwise
|
||||
*/
|
||||
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
|
||||
disableConnectionReuseIfNecessary();
|
||||
HttpURLConnection urlConnection = null;
|
||||
BufferedOutputStream out = null;
|
||||
BufferedInputStream in = null;
|
||||
|
||||
try {
|
||||
final URL url = new URL(urlString);
|
||||
urlConnection = (HttpURLConnection) url.openConnection();
|
||||
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
|
||||
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
|
||||
|
||||
int b;
|
||||
while ((b = in.read()) != -1) {
|
||||
out.write(b);
|
||||
}
|
||||
return true;
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Error in downloadBitmap - " + e);
|
||||
} finally {
|
||||
if (urlConnection != null) {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
try {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
} catch (final IOException e) {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for bug pre-Froyo, see here for more info:
|
||||
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
|
||||
*/
|
||||
public static void disableConnectionReuseIfNecessary() {
|
||||
// HTTP connection reuse which was buggy pre-froyo
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
|
||||
System.setProperty("http.keepAlive", "false");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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 org.nativescript.widgets.image;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Build;
|
||||
import java.io.FileDescriptor;
|
||||
|
||||
/**
|
||||
* A simple subclass of {@link Worker} that resize images given a target width
|
||||
* and height. Useful for when the input images might be too large to simply load directly into
|
||||
* memory.
|
||||
*/
|
||||
public abstract class Resizer extends Worker {
|
||||
protected Resizer(Context context) {
|
||||
super(context);
|
||||
}
|
||||
/**
|
||||
* Decode and sample down a bitmap from resources to the requested width and height.
|
||||
*
|
||||
* @param res The resources object containing the image data
|
||||
* @param resId The resource id of the image data
|
||||
* @param reqWidth The requested width of the resulting bitmap
|
||||
* @param reqHeight The requested height of the resulting bitmap
|
||||
* @param cache The Cache used to find candidate bitmaps for use with inBitmap
|
||||
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
|
||||
* that are equal to or greater than the requested width and height
|
||||
*/
|
||||
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
|
||||
int reqWidth, int reqHeight, Cache cache) {
|
||||
|
||||
// BEGIN_INCLUDE (read_bitmap_dimensions)
|
||||
// First decode with inJustDecodeBounds=true to check dimensions
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeResource(res, resId, options);
|
||||
|
||||
// If requested width/height were not specified - decode in full size.
|
||||
if (reqWidth > 0 && reqHeight > 0) {
|
||||
// Calculate inSampleSize
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
||||
}
|
||||
else {
|
||||
options.inSampleSize = 1;
|
||||
}
|
||||
|
||||
// END_INCLUDE (read_bitmap_dimensions)
|
||||
|
||||
// If we're running on Honeycomb or newer, try to use inBitmap
|
||||
if (Utils.hasHoneycomb()) {
|
||||
addInBitmapOptions(options, cache);
|
||||
}
|
||||
|
||||
// Decode bitmap with inSampleSize set
|
||||
options.inJustDecodeBounds = false;
|
||||
return BitmapFactory.decodeResource(res, resId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and sample down a bitmap from a file to the requested width and height.
|
||||
*
|
||||
* @param filename The full path of the file to decode
|
||||
* @param reqWidth The requested width of the resulting bitmap
|
||||
* @param reqHeight The requested height of the resulting bitmap
|
||||
* @param cache The Cache used to find candidate bitmaps for use with inBitmap
|
||||
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
|
||||
* that are equal to or greater than the requested width and height
|
||||
*/
|
||||
public static Bitmap decodeSampledBitmapFromFile(String filename,
|
||||
int reqWidth, int reqHeight, Cache cache) {
|
||||
|
||||
// First decode with inJustDecodeBounds=true to check dimensions
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(filename, options);
|
||||
|
||||
// If requested width/height were not specified - decode in full size.
|
||||
if (reqWidth > 0 && reqHeight > 0) {
|
||||
// Calculate inSampleSize
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
||||
}
|
||||
else {
|
||||
options.inSampleSize = 1;
|
||||
}
|
||||
|
||||
// If we're running on Honeycomb or newer, try to use inBitmap
|
||||
if (Utils.hasHoneycomb()) {
|
||||
addInBitmapOptions(options, cache);
|
||||
}
|
||||
|
||||
// Decode bitmap with inSampleSize set
|
||||
options.inJustDecodeBounds = false;
|
||||
return BitmapFactory.decodeFile(filename, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and sample down a bitmap from a file input stream to the requested width and height.
|
||||
*
|
||||
* @param fileDescriptor The file descriptor to read from
|
||||
* @param reqWidth The requested width of the resulting bitmap
|
||||
* @param reqHeight The requested height of the resulting bitmap
|
||||
* @param cache The Cache used to find candidate bitmaps for use with inBitmap
|
||||
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
|
||||
* that are equal to or greater than the requested width and height
|
||||
*/
|
||||
public static Bitmap decodeSampledBitmapFromDescriptor(
|
||||
FileDescriptor fileDescriptor, int reqWidth, int reqHeight, Cache cache) {
|
||||
|
||||
// First decode with inJustDecodeBounds=true to check dimensions
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
|
||||
|
||||
// If requested width/height were not specified - decode in full size.
|
||||
if (reqWidth > 0 && reqHeight > 0) {
|
||||
// Calculate inSampleSize
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
||||
}
|
||||
else {
|
||||
options.inSampleSize = 1;
|
||||
}
|
||||
|
||||
// Decode bitmap with inSampleSize set
|
||||
options.inJustDecodeBounds = false;
|
||||
|
||||
// If we're running on Honeycomb or newer, try to use inBitmap
|
||||
if (Utils.hasHoneycomb()) {
|
||||
addInBitmapOptions(options, cache);
|
||||
}
|
||||
|
||||
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
|
||||
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
|
||||
* the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap
|
||||
* having a width and height equal to or larger than the requested width and height.
|
||||
*
|
||||
* @param options An options object with out* params already populated (run through a decode*
|
||||
* method with inJustDecodeBounds==true
|
||||
* @param reqWidth The requested width of the resulting bitmap
|
||||
* @param reqHeight The requested height of the resulting bitmap
|
||||
* @return The value to be used for inSampleSize
|
||||
*/
|
||||
public static int calculateInSampleSize(BitmapFactory.Options options,
|
||||
int reqWidth, int reqHeight) {
|
||||
// BEGIN_INCLUDE (calculate_sample_size)
|
||||
// Raw height and width of image
|
||||
final int height = options.outHeight;
|
||||
final int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
|
||||
final int halfHeight = height / 2;
|
||||
final int halfWidth = width / 2;
|
||||
|
||||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||
// height and width larger than the requested height and width.
|
||||
while ((halfHeight / inSampleSize) > reqHeight
|
||||
&& (halfWidth / inSampleSize) > reqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
|
||||
// This offers some additional logic in case the image has a strange
|
||||
// aspect ratio. For example, a panorama may have a much larger
|
||||
// width than height. In these cases the total pixels might still
|
||||
// end up being too large to fit comfortably in memory, so we should
|
||||
// be more aggressive with sample down the image (=larger inSampleSize).
|
||||
|
||||
long totalPixels = width * height / inSampleSize;
|
||||
|
||||
// Anything more than 2x the requested pixels we'll sample down further
|
||||
final long totalReqPixelsCap = reqWidth * reqHeight * 2;
|
||||
|
||||
while (totalPixels > totalReqPixelsCap) {
|
||||
inSampleSize *= 2;
|
||||
totalPixels /= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
// END_INCLUDE (calculate_sample_size)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private static void addInBitmapOptions(BitmapFactory.Options options, Cache cache) {
|
||||
//BEGIN_INCLUDE(add_bitmap_options)
|
||||
// inBitmap only works with mutable bitmaps so force the decoder to
|
||||
// return mutable bitmaps.
|
||||
options.inMutable = true;
|
||||
|
||||
if (cache != null) {
|
||||
// Try and find a bitmap to use for inBitmap
|
||||
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
|
||||
|
||||
if (inBitmap != null) {
|
||||
options.inBitmap = inBitmap;
|
||||
}
|
||||
}
|
||||
//END_INCLUDE(add_bitmap_options)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 org.nativescript.widgets.image;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.StrictMode;
|
||||
|
||||
/**
|
||||
* Class containing some static utility methods.
|
||||
*/
|
||||
public class Utils {
|
||||
private Utils() {};
|
||||
|
||||
@TargetApi(VERSION_CODES.HONEYCOMB)
|
||||
public static void enableStrictMode() {
|
||||
if (Utils.hasGingerbread()) {
|
||||
StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
|
||||
new StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog();
|
||||
StrictMode.VmPolicy.Builder vmPolicyBuilder =
|
||||
new StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog();
|
||||
|
||||
if (Utils.hasHoneycomb()) {
|
||||
threadPolicyBuilder.penaltyFlashScreen();
|
||||
}
|
||||
StrictMode.setThreadPolicy(threadPolicyBuilder.build());
|
||||
StrictMode.setVmPolicy(vmPolicyBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasFroyo() {
|
||||
// Can use static final constants like FROYO, declared in later versions
|
||||
// of the OS since they are inlined at compile time. This is guaranteed behavior.
|
||||
return Build.VERSION.SDK_INT >= VERSION_CODES.FROYO;
|
||||
}
|
||||
|
||||
public static boolean hasGingerbread() {
|
||||
return Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD;
|
||||
}
|
||||
|
||||
public static boolean hasHoneycomb() {
|
||||
return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
|
||||
}
|
||||
|
||||
public static boolean hasHoneycombMR1() {
|
||||
return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1;
|
||||
}
|
||||
|
||||
public static boolean hasJellyBean() {
|
||||
return Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
|
||||
}
|
||||
|
||||
public static boolean hasKitKat() {
|
||||
return Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
/*
|
||||
* 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 org.nativescript.widgets.image;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.TransitionDrawable;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* This class wraps up completing some arbitrary long running work when loading a bitmap to an
|
||||
* ImageView. It handles things like using a memory and disk cache, running the work in a background
|
||||
* thread and setting a placeholder image.
|
||||
*/
|
||||
public abstract class Worker {
|
||||
|
||||
static final String TAG = "JS";
|
||||
private static final int FADE_IN_TIME = 200;
|
||||
|
||||
private Cache mCache;
|
||||
private Bitmap mLoadingBitmap;
|
||||
private boolean mFadeInBitmap = true;
|
||||
private boolean mExitTasksEarly = false;
|
||||
private final Object mPauseWorkLock = new Object();
|
||||
|
||||
protected boolean mPauseWork = false;
|
||||
protected Resources mResources;
|
||||
|
||||
private static final int MESSAGE_CLEAR = 0;
|
||||
private static final int MESSAGE_INIT_DISK_CACHE = 1;
|
||||
private static final int MESSAGE_FLUSH = 2;
|
||||
private static final int MESSAGE_CLOSE = 3;
|
||||
|
||||
protected static int debuggable = -1;
|
||||
|
||||
protected Worker(Context context) {
|
||||
mResources = context.getResources();
|
||||
|
||||
// Negative means not initialized.
|
||||
if (debuggable < 0) {
|
||||
try {
|
||||
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA);
|
||||
android.os.Bundle bundle = ai.metaData;
|
||||
Boolean debugLayouts = bundle != null ? bundle.getBoolean("debugImageCache", false) : false;
|
||||
debuggable = debugLayouts ? 1 : 0;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
debuggable = 0;
|
||||
Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage());
|
||||
} catch (NullPointerException e) {
|
||||
debuggable = 0;
|
||||
Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an image specified by the data parameter into an ImageView (override
|
||||
* {@link Worker#processBitmap(Object, int, int)} to define the processing logic). A memory and
|
||||
* disk cache will be used if an {@link Cache} has been added using
|
||||
* {@link Worker#addImageCache(Cache)}. If the
|
||||
* image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
|
||||
* will be created to asynchronously load the bitmap.
|
||||
*
|
||||
* @param data The URL of the image to download.
|
||||
* @param imageView The ImageView to bind the downloaded image to.
|
||||
* @param listener A listener that will be called back once the image has been loaded.
|
||||
*/
|
||||
public void loadImage(Object data, ImageView imageView, int decodeWidth, int decodeHeight, boolean useCache, boolean async, OnImageLoadedListener listener) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BitmapDrawable value = null;
|
||||
String dataString = String.valueOf(data);
|
||||
|
||||
if (mCache != null && useCache) {
|
||||
value = mCache.getBitmapFromMemCache(dataString);
|
||||
}
|
||||
|
||||
if (value == null && !async) {
|
||||
// Decode sync.
|
||||
Bitmap bitmap = processBitmap(data, decodeWidth, decodeHeight);
|
||||
if (bitmap != null) {
|
||||
value = new BitmapDrawable(mResources, bitmap);
|
||||
if (mCache != null && useCache) {
|
||||
// Don't add Images loaded from Resources to disk cache.
|
||||
boolean addToDiskCache = !(data instanceof Number);
|
||||
mCache.addBitmapToCache(dataString, value, addToDiskCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
// Bitmap found in memory cache
|
||||
imageView.setImageDrawable(value);
|
||||
if (listener != null) {
|
||||
listener.onImageLoaded(true);
|
||||
}
|
||||
} else if (cancelPotentialWork(data, imageView)) {
|
||||
final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView, decodeWidth, decodeHeight, useCache, listener);
|
||||
final AsyncDrawable asyncDrawable =
|
||||
new AsyncDrawable(mResources, mLoadingBitmap, task);
|
||||
imageView.setImageDrawable(asyncDrawable);
|
||||
|
||||
// NOTE: This uses a custom version of AsyncTask that has been pulled from the
|
||||
// framework and slightly modified. Refer to the docs at the top of the class
|
||||
// for more info on what was changed.
|
||||
task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set placeholder bitmap that shows when the the background thread is running.
|
||||
*
|
||||
* @param bitmap
|
||||
*/
|
||||
public void setLoadingImage(Bitmap bitmap) {
|
||||
mLoadingBitmap = bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set placeholder bitmap that shows when the the background thread is running.
|
||||
*
|
||||
* @param resId
|
||||
*/
|
||||
public void setLoadingImage(int resId) {
|
||||
mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link Cache} to this {@link Worker} to handle disk and memory bitmap
|
||||
* caching. ImageCahce should be initialized before it is passed as parameter.
|
||||
*/
|
||||
public void addImageCache(Cache imageCache) {
|
||||
this.mCache = imageCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to true, the image will fade-in once it has been loaded by the background thread.
|
||||
*/
|
||||
public void setImageFadeIn(boolean fadeIn) {
|
||||
mFadeInBitmap = fadeIn;
|
||||
}
|
||||
|
||||
public void setExitTasksEarly(boolean exitTasksEarly) {
|
||||
mExitTasksEarly = exitTasksEarly;
|
||||
setPauseWork(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses should override this to define any processing or work that must happen to produce
|
||||
* the final bitmap. This will be executed in a background thread and be long running. For
|
||||
* example, you could resize a large bitmap here, or pull down an image from the network.
|
||||
*
|
||||
* @param data The data to identify which image to process, as provided by
|
||||
* {@link Worker#loadImage(Object, ImageView)}
|
||||
* @return The processed bitmap
|
||||
*/
|
||||
protected abstract Bitmap processBitmap(Object data, int decodeWidth, int decodeHeight);
|
||||
|
||||
/**
|
||||
* @return The {@link Cache} object currently being used by this Worker.
|
||||
*/
|
||||
protected Cache getImageCache() {
|
||||
return mCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any pending work attached to the provided ImageView.
|
||||
* @param imageView
|
||||
*/
|
||||
public static void cancelWork(ImageView imageView) {
|
||||
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
|
||||
if (bitmapWorkerTask != null) {
|
||||
bitmapWorkerTask.cancel(true);
|
||||
if (debuggable > 0) {
|
||||
final Object bitmapData = bitmapWorkerTask.mData;
|
||||
Log.v(TAG, "cancelWork - cancelled work for " + bitmapData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current work has been canceled or if there was no work in
|
||||
* progress on this image view.
|
||||
* Returns false if the work in progress deals with the same data. The work is not
|
||||
* stopped in that case.
|
||||
*/
|
||||
public static boolean cancelPotentialWork(Object data, ImageView imageView) {
|
||||
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
|
||||
|
||||
if (bitmapWorkerTask != null) {
|
||||
final Object bitmapData = bitmapWorkerTask.mData;
|
||||
if (bitmapData == null || !bitmapData.equals(data)) {
|
||||
bitmapWorkerTask.cancel(true);
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "cancelPotentialWork - cancelled work for " + data);
|
||||
}
|
||||
} else {
|
||||
// The same work is already in progress.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param imageView Any imageView
|
||||
* @return Retrieve the currently active work task (if any) associated with this imageView.
|
||||
* null if there is no such task.
|
||||
*/
|
||||
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
|
||||
if (imageView != null) {
|
||||
final Drawable drawable = imageView.getDrawable();
|
||||
if (drawable instanceof AsyncDrawable) {
|
||||
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
||||
return asyncDrawable.getBitmapWorkerTask();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual AsyncTask that will asynchronously process the image.
|
||||
*/
|
||||
private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> {
|
||||
private int mDecodeWidth;
|
||||
private int mDecodeHeight;
|
||||
private Object mData;
|
||||
private boolean mCacheImage;
|
||||
private final WeakReference<ImageView> imageViewReference;
|
||||
private final OnImageLoadedListener mOnImageLoadedListener;
|
||||
|
||||
public BitmapWorkerTask(Object data, ImageView imageView, int decodeWidth, int decodeHeight, boolean cacheImage) {
|
||||
this(data, imageView, decodeWidth, decodeHeight, cacheImage, null);
|
||||
}
|
||||
|
||||
public BitmapWorkerTask(Object data, ImageView imageView, int decodeWidth, int decodeHeight, boolean cacheImage, OnImageLoadedListener listener) {
|
||||
mDecodeWidth = decodeWidth;
|
||||
mDecodeHeight = decodeHeight;
|
||||
mCacheImage = cacheImage;
|
||||
mData = data;
|
||||
imageViewReference = new WeakReference<ImageView>(imageView);
|
||||
mOnImageLoadedListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Background processing.
|
||||
*/
|
||||
@Override
|
||||
protected BitmapDrawable doInBackground(Void... params) {
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "doInBackground - starting work");
|
||||
}
|
||||
|
||||
final String dataString = String.valueOf(mData);
|
||||
Bitmap bitmap = null;
|
||||
BitmapDrawable drawable = null;
|
||||
|
||||
// Wait here if work is paused and the task is not cancelled
|
||||
synchronized (mPauseWorkLock) {
|
||||
while (mPauseWork && !isCancelled()) {
|
||||
try {
|
||||
mPauseWorkLock.wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// If the image cache is available and this task has not been cancelled by another
|
||||
// thread and the ImageView that was originally bound to this task is still bound back
|
||||
// to this task and our "exit early" flag is not set then try and fetch the bitmap from
|
||||
// the cache
|
||||
if (mCache != null && !isCancelled() && getAttachedImageView() != null
|
||||
&& !mExitTasksEarly && mCacheImage) {
|
||||
bitmap = mCache.getBitmapFromDiskCache(dataString);
|
||||
}
|
||||
|
||||
// If the bitmap was not found in the cache and this task has not been cancelled by
|
||||
// another thread and the ImageView that was originally bound to this task is still
|
||||
// bound back to this task and our "exit early" flag is not set, then call the main
|
||||
// process method (as implemented by a subclass)
|
||||
if (bitmap == null && !isCancelled() && getAttachedImageView() != null
|
||||
&& !mExitTasksEarly) {
|
||||
bitmap = processBitmap(mData, mDecodeWidth, mDecodeHeight);
|
||||
}
|
||||
|
||||
// If the bitmap was processed and the image cache is available, then add the processed
|
||||
// bitmap to the cache for future use. Note we don't check if the task was cancelled
|
||||
// here, if it was, and the thread is still running, we may as well add the processed
|
||||
// bitmap to our cache as it might be used again in the future
|
||||
if (bitmap != null) {
|
||||
drawable = new BitmapDrawable(mResources, bitmap);
|
||||
if (mCache != null && mCacheImage) {
|
||||
// Don't add Images loaded from Resources to disk cache.
|
||||
boolean addToDiskCache = !(mData instanceof Number);
|
||||
mCache.addBitmapToCache(dataString, drawable, addToDiskCache);
|
||||
}
|
||||
}
|
||||
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "doInBackground - finished work");
|
||||
}
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the image is processed, associates it to the imageView
|
||||
*/
|
||||
@Override
|
||||
protected void onPostExecute(BitmapDrawable value) {
|
||||
boolean success = false;
|
||||
// if cancel was called on this task or the "exit early" flag is set then we're done
|
||||
if (isCancelled() || mExitTasksEarly) {
|
||||
value = null;
|
||||
}
|
||||
|
||||
final ImageView imageView = getAttachedImageView();
|
||||
if (value != null && imageView != null) {
|
||||
if (debuggable > 0) {
|
||||
Log.v(TAG, "onPostExecute - setting bitmap");
|
||||
}
|
||||
success = true;
|
||||
setImageDrawable(imageView, value);
|
||||
}
|
||||
if (mOnImageLoadedListener != null) {
|
||||
mOnImageLoadedListener.onImageLoaded(success);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(BitmapDrawable value) {
|
||||
super.onCancelled(value);
|
||||
synchronized (mPauseWorkLock) {
|
||||
mPauseWorkLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ImageView associated with this task as long as the ImageView's task still
|
||||
* points to this task as well. Returns null otherwise.
|
||||
*/
|
||||
private ImageView getAttachedImageView() {
|
||||
final ImageView imageView = imageViewReference.get();
|
||||
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
|
||||
|
||||
if (this == bitmapWorkerTask) {
|
||||
return imageView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface definition for callback on image loaded successfully.
|
||||
*/
|
||||
public interface OnImageLoadedListener {
|
||||
|
||||
/**
|
||||
* Called once the image has been loaded.
|
||||
* @param success True if the image was loaded successfully, false if
|
||||
* there was an error.
|
||||
*/
|
||||
void onImageLoaded(boolean success);
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom Drawable that will be attached to the imageView while the work is in progress.
|
||||
* Contains a reference to the actual worker task, so that it can be stopped if a new binding is
|
||||
* required, and makes sure that only the last started worker process can bind its result,
|
||||
* independently of the finish order.
|
||||
*/
|
||||
private static class AsyncDrawable extends BitmapDrawable {
|
||||
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
|
||||
|
||||
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
|
||||
super(res, bitmap);
|
||||
bitmapWorkerTaskReference =
|
||||
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
|
||||
}
|
||||
|
||||
public BitmapWorkerTask getBitmapWorkerTask() {
|
||||
return bitmapWorkerTaskReference.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the processing is complete and the final drawable should be
|
||||
* set on the ImageView.
|
||||
*
|
||||
* @param imageView
|
||||
* @param drawable
|
||||
*/
|
||||
private void setImageDrawable(ImageView imageView, Drawable drawable) {
|
||||
if (mFadeInBitmap) {
|
||||
// Transition drawable with a transparent drawable and the final drawable
|
||||
final TransitionDrawable td =
|
||||
new TransitionDrawable(new Drawable[] {
|
||||
new ColorDrawable(0),
|
||||
drawable
|
||||
});
|
||||
// Set background to loading bitmap
|
||||
imageView.setBackgroundDrawable(
|
||||
new BitmapDrawable(mResources, mLoadingBitmap));
|
||||
|
||||
imageView.setImageDrawable(td);
|
||||
td.startTransition(FADE_IN_TIME);
|
||||
} else {
|
||||
imageView.setImageDrawable(drawable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause any ongoing background work. This can be used as a temporary
|
||||
* measure to improve performance. For example background work could
|
||||
* be paused when a ListView or GridView is being scrolled using a
|
||||
* {@link android.widget.AbsListView.OnScrollListener} to keep
|
||||
* scrolling smooth.
|
||||
* <p>
|
||||
* If work is paused, be sure setPauseWork(false) is called again
|
||||
* before your fragment or activity is destroyed (for example during
|
||||
* {@link android.app.Activity#onPause()}), or there is a risk the
|
||||
* background thread will never finish.
|
||||
*/
|
||||
public void setPauseWork(boolean pauseWork) {
|
||||
synchronized (mPauseWorkLock) {
|
||||
mPauseWork = pauseWork;
|
||||
if (!mPauseWork) {
|
||||
mPauseWorkLock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CacheAsyncTask extends AsyncTask<Object, Void, Void> {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Object... params) {
|
||||
switch ((Integer)params[0]) {
|
||||
case MESSAGE_CLEAR:
|
||||
clearCacheInternal();
|
||||
break;
|
||||
case MESSAGE_INIT_DISK_CACHE:
|
||||
initDiskCacheInternal();
|
||||
break;
|
||||
case MESSAGE_FLUSH:
|
||||
flushCacheInternal();
|
||||
break;
|
||||
case MESSAGE_CLOSE:
|
||||
closeCacheInternal();
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void initDiskCacheInternal() {
|
||||
if (mCache != null) {
|
||||
mCache.initDiskCache();
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearCacheInternal() {
|
||||
if (mCache != null) {
|
||||
mCache.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
protected void flushCacheInternal() {
|
||||
if (mCache != null) {
|
||||
mCache.flush();
|
||||
}
|
||||
}
|
||||
|
||||
protected void closeCacheInternal() {
|
||||
if (mCache != null) {
|
||||
mCache.close();
|
||||
mCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void initCache() {
|
||||
new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
new CacheAsyncTask().execute(MESSAGE_CLEAR);
|
||||
}
|
||||
|
||||
public void flushCache() {
|
||||
new CacheAsyncTask().execute(MESSAGE_FLUSH);
|
||||
}
|
||||
|
||||
public void closeCache() {
|
||||
new CacheAsyncTask().execute(MESSAGE_CLOSE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package org.nativescript.widgets;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
/**
|
||||
* Created by hhristov on 8/23/16.
|
||||
*/
|
||||
|
||||
public class ViewHelper {
|
||||
private ViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
static final int version = android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
public static int getMinWidth(android.view.View view) {
|
||||
return view.getMinimumWidth();
|
||||
}
|
||||
public static void setMinWidth(android.view.View view, int value) {
|
||||
view.setMinimumWidth(value);
|
||||
}
|
||||
|
||||
public static int getMinHeight(android.view.View view) {
|
||||
return view.getMinimumHeight();
|
||||
}
|
||||
public static void setMinHeight(android.view.View view, int value) {
|
||||
view.setMinimumHeight(value);
|
||||
}
|
||||
|
||||
public static int getWidth(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params != null) {
|
||||
return params.width;
|
||||
}
|
||||
|
||||
return ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
}
|
||||
public static void setWidth(android.view.View view, int value) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
params.width = value;
|
||||
}
|
||||
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
|
||||
public static int getHeight(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params != null) {
|
||||
return params.height;
|
||||
}
|
||||
|
||||
return ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
}
|
||||
public static void setHeight(android.view.View view, int value) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
params.height = value;
|
||||
}
|
||||
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
|
||||
public static Rect getMargin(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
return new Rect(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin);
|
||||
}
|
||||
|
||||
return new Rect();
|
||||
}
|
||||
public static void setMargin(android.view.View view, int left, int top, int right, int bottom) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
// Initialize if empty.
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
}
|
||||
|
||||
// Set margins only if params are of the correct type.
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
lp.leftMargin = left;
|
||||
lp.topMargin = top;
|
||||
lp.rightMargin = right;
|
||||
lp.bottomMargin = bottom;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getMarginLeft(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
return lp.leftMargin;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
public static void setMarginLeft(android.view.View view, int value) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
// Initialize if empty.
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
}
|
||||
|
||||
// Set margins only if params are of the correct type.
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
lp.leftMargin = value;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getMarginTop(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
return lp.topMargin;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
public static void setMarginTop(android.view.View view, int value) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
// Initialize if empty.
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
}
|
||||
|
||||
// Set margins only if params are of the correct type.
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
lp.topMargin = value;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getMarginRight(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
return lp.rightMargin;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
public static void setMarginRight(android.view.View view, int value) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
// Initialize if empty.
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
}
|
||||
|
||||
// Set margins only if params are of the correct type.
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
lp.rightMargin = value;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getMarginBottom(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
return lp.bottomMargin;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
public static void setMarginBottom(android.view.View view, int value) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
// Initialize if empty.
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
}
|
||||
|
||||
// Set margins only if params are of the correct type.
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
|
||||
lp.bottomMargin = value;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getHorizontalAlighment(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof FrameLayout.LayoutParams) {
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params;
|
||||
if (Gravity.isHorizontal(lp.gravity)) {
|
||||
switch (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
|
||||
case Gravity.LEFT:
|
||||
return "left";
|
||||
case Gravity.CENTER:
|
||||
return "center";
|
||||
case Gravity.RIGHT:
|
||||
return "right";
|
||||
case Gravity.FILL_HORIZONTAL:
|
||||
return "stretch";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "stretch";
|
||||
}
|
||||
public static void setHorizontalAlighment(android.view.View view, String value) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
// Initialize if empty.
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
}
|
||||
|
||||
// Set margins only if params are of the correct type.
|
||||
if (params instanceof FrameLayout.LayoutParams) {
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params;
|
||||
switch (value) {
|
||||
case "left":
|
||||
lp.gravity = Gravity.LEFT | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK);
|
||||
break;
|
||||
case "center":
|
||||
lp.gravity = Gravity.CENTER_HORIZONTAL | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK);
|
||||
break;
|
||||
case "right":
|
||||
lp.gravity = Gravity.RIGHT | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK);
|
||||
break;
|
||||
case "stretch":
|
||||
lp.gravity = Gravity.FILL_HORIZONTAL | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK);
|
||||
break;
|
||||
}
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getVerticalAlignment(android.view.View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof FrameLayout.LayoutParams) {
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params;
|
||||
if (Gravity.isHorizontal(lp.gravity)) {
|
||||
switch (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK) {
|
||||
case Gravity.TOP:
|
||||
return "top";
|
||||
case Gravity.CENTER:
|
||||
return "center";
|
||||
case Gravity.BOTTOM:
|
||||
return "bottom";
|
||||
case Gravity.FILL_VERTICAL:
|
||||
return "stretch";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "stretch";
|
||||
}
|
||||
public static void setVerticalAlignment(android.view.View view, String value){
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
// Initialize if empty.
|
||||
if (params == null) {
|
||||
params = new CommonLayoutParams();
|
||||
}
|
||||
|
||||
// Set margins only if params are of the correct type.
|
||||
if (params instanceof FrameLayout.LayoutParams) {
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params;
|
||||
switch (value) {
|
||||
case "top":
|
||||
lp.gravity = Gravity.TOP | (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK);
|
||||
break;
|
||||
case "center":
|
||||
lp.gravity = Gravity.CENTER_VERTICAL | (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK);
|
||||
break;
|
||||
case "bottom":
|
||||
lp.gravity = Gravity.BOTTOM| (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK);
|
||||
break;
|
||||
case "stretch":
|
||||
lp.gravity = Gravity.FILL_VERTICAL | (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK);
|
||||
break;
|
||||
}
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
public static Rect getPadding(android.view.View view) {
|
||||
return new Rect(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom());
|
||||
}
|
||||
public static void setPadding(android.view.View view, int left, int top, int right, int bottom) {
|
||||
view.setPadding(left, top, right, bottom);
|
||||
}
|
||||
|
||||
public static int getPaddingLeft(android.view.View view) {
|
||||
return view.getPaddingLeft();
|
||||
}
|
||||
public static void setPaddingLeft(android.view.View view, int value) {
|
||||
view.setPadding(value, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom());
|
||||
}
|
||||
|
||||
public static int getPaddingTop(android.view.View view) {
|
||||
return view.getPaddingTop();
|
||||
}
|
||||
public static void setPaddingTop(android.view.View view, int value) {
|
||||
view.setPadding(view.getPaddingLeft(), value, view.getPaddingRight(), view.getPaddingBottom());
|
||||
}
|
||||
|
||||
public static int getPaddingRight(android.view.View view) {
|
||||
return view.getPaddingRight();
|
||||
}
|
||||
public static void setPaddingRight(android.view.View view, int value) {
|
||||
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), value, view.getPaddingBottom());
|
||||
}
|
||||
|
||||
public static int getPaddingBottom(android.view.View view) {
|
||||
return view.getPaddingBottom();
|
||||
}
|
||||
public static void setPaddingBottom(android.view.View view, int value) {
|
||||
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), value);
|
||||
}
|
||||
|
||||
public static float getRotate(android.view.View view) {
|
||||
return view.getRotation();
|
||||
}
|
||||
public static void setRotate(android.view.View view, float value) {
|
||||
view.setRotation(value);
|
||||
}
|
||||
|
||||
public static float getScaleX(android.view.View view) {
|
||||
return view.getScaleX();
|
||||
}
|
||||
public static void setScaleX(android.view.View view, float value) {
|
||||
view.setScaleX(value);
|
||||
}
|
||||
|
||||
public static float getScaleY(android.view.View view) {
|
||||
return view.getScaleY();
|
||||
}
|
||||
public static void setScaleY(android.view.View view, float value) {
|
||||
view.setScaleY(value);
|
||||
}
|
||||
|
||||
public static float getTranslateX(android.view.View view) {
|
||||
return view.getTranslationX();
|
||||
}
|
||||
public static void setTranslateX(android.view.View view, float value) {
|
||||
view.setTranslationX(value);
|
||||
}
|
||||
|
||||
public static float getTranslateY(android.view.View view) {
|
||||
return view.getTranslationY();
|
||||
}
|
||||
public static void setTranslateY(android.view.View view, float value) {
|
||||
view.setTranslationY(value);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
public static float getZIndex(android.view.View view) {
|
||||
if (ViewHelper.version >= 21) {
|
||||
return view.getZ();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
public static void setZIndex(android.view.View view, float value) {
|
||||
if (ViewHelper.version >= 21) {
|
||||
view.setZ(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user