(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() {
+ }
+
+ /**
+ * Runs on the UI thread after {@link #doInBackground}. The
+ * specified result is the value returned by {@link #doInBackground}.
+ *
+ * This method won't be invoked if the task was cancelled.
+ *
+ * @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) {
+ }
+
+ /**
+ * Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+ * {@link #doInBackground(Object[])} has finished.
+ *
+ * The default implementation simply invokes {@link #onCancelled()} and
+ * ignores the result. If you write your own implementation, do not call
+ * super.onCancelled(result)
.
+ *
+ * @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();
+ }
+
+ /**
+ * Applications should preferably override {@link #onCancelled(Object)}.
+ * This method is invoked by the default implementation of
+ * {@link #onCancelled(Object)}.
+ *
+ * Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+ * {@link #doInBackground(Object[])} has finished.
+ *
+ * @see #onCancelled(Object)
+ * @see #cancel(boolean)
+ * @see #isCancelled()
+ */
+ protected void onCancelled() {
+ }
+
+ /**
+ * Returns true 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 true if task was cancelled before it completed
+ *
+ * @see #cancel(boolean)
+ */
+ public final boolean isCancelled() {
+ return mCancelled.get();
+ }
+
+ /**
+ * 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 cancel is called,
+ * this task should never run. If the task has already started,
+ * then the mayInterruptIfRunning parameter determines
+ * whether the thread executing this task should be interrupted in
+ * an attempt to stop the task.
+ *
+ * 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.
+ *
+ * @param mayInterruptIfRunning true if the thread executing this
+ * task should be interrupted; otherwise, in-progress tasks are allowed
+ * to complete.
+ *
+ * @return false if the task could not be cancelled,
+ * typically because it has already completed normally;
+ * true 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.
+ *
+ * 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.
+ *
+ *
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 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.
+ *
+ * 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.
+ *
+ *
Warning: Allowing multiple tasks to run in parallel from
+ * a thread pool is generally not 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}.
+ *
+ *
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 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(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 implements Callable {
+ Params[] mParams;
+ }
+
+ @SuppressWarnings({"RawUseOfParameterizedType"})
+ private static class AsyncTaskResult {
+ final AsyncTask mTask;
+ final Data[] mData;
+
+ AsyncTaskResult(AsyncTask task, Data... data) {
+ mTask = task;
+ mData = data;
+ }
+ }
+}
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/java/util/DiskLruCache.java b/opensrp-sdidtk/src/main/java/util/DiskLruCache.java
new file mode 100644
index 000000000..e460d1eae
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/util/DiskLruCache.java
@@ -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 util;
+
+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.
+ *
+ * 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.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ * When an entry is being created it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ * When an entry is being edited , it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
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 lruEntries
+ = new LinkedHashMap(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[] 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());
+ private final Callable cleanupCallable = new Callable() {
+ @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 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(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+// Map.Entry toEvict = lruEntries.eldest();
+ final Map.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");
+ }
+ }
+}
diff --git a/opensrp-sdidtk/src/main/java/util/ImageCache.java b/opensrp-sdidtk/src/main/java/util/ImageCache.java
new file mode 100644
index 000000000..98c17b90b
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/util/ImageCache.java
@@ -0,0 +1,738 @@
+/*
+ * 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 util;
+
+import android.annotation.TargetApi;
+import android.app.Fragment;
+import android.app.FragmentManager;
+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.Bundle;
+import android.os.Environment;
+import android.os.StatFs;
+import android.util.Log;
+import android.util.LruCache;
+
+import org.ei.opensrp.BuildConfig;
+
+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 ImageWorker} class and its subclasses. Use
+ * to get an instance of this
+ * class, although usually a cache should be added directly to an {@link ImageWorker} by calling
+ * .
+ */
+public class ImageCache {
+ private static final String TAG = "ImageCache";
+
+ // 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 DiskLruCache mDiskLruCache;
+ private LruCache mMemoryCache;
+ private ImageCacheParams mCacheParams;
+ private final Object mDiskCacheLock = new Object();
+ private boolean mDiskCacheStarting = true;
+
+ private Set> mReusableBitmaps;
+
+ /**
+ * Create a new ImageCache object using the specified parameters. This should not be
+ * called directly by other classes, instead use
+ * to fetch an ImageCache
+ * instance.
+ *
+ * @param cacheParams The cache parameters to use to initialize the cache
+ */
+ private ImageCache(ImageCacheParams cacheParams) {
+ init(cacheParams);
+ }
+
+ /**
+ * Return an {@link ImageCache} instance. A {@link RetainFragment} is used to retain the
+ * ImageCache object across configuration changes such as a change in device orientation.
+ *
+ * @param fragmentManager The fragment manager to use when dealing with the retained fragment.
+ * @param cacheParams The cache parameters to use if the ImageCache needs instantiation.
+ * @return An existing retained ImageCache object or a new one if one did not exist
+ */
+ public static ImageCache getInstance(
+ FragmentManager fragmentManager, ImageCacheParams cacheParams) {
+
+ // Search for, or create an instance of the non-UI RetainFragment
+ final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
+
+ // See if we already have an ImageCache stored in RetainFragment
+ ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
+
+ // No existing ImageCache, create one and store it in RetainFragment
+ if (imageCache == null) {
+ imageCache = new ImageCache(cacheParams);
+ mRetainFragment.setObject(imageCache);
+ }
+
+ return imageCache;
+ }
+
+ /**
+ * Initialize the cache, providing all parameters.
+ *
+ * @param cacheParams The cache parameters to initialize the cache
+ */
+ private void init(ImageCacheParams cacheParams) {
+ mCacheParams = cacheParams;
+
+ //BEGIN_INCLUDE(init_memory_cache)
+ // Set up memory cache
+ if (mCacheParams.memoryCacheEnabled) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Memory cache created (size = " + mCacheParams.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>());
+ }
+
+ mMemoryCache = new LruCache(mCacheParams.memCacheSize) {
+
+ /**
+ * Notify the removed entry that is no longer being cached
+ */
+ @Override
+ protected void entryRemoved(boolean evicted, String key,
+ BitmapDrawable oldValue, BitmapDrawable newValue) {
+ if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
+ // The removed entry is a recycling drawable, so notify it
+ // that it has been removed from the memory cache
+ ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
+ } else {
+ // The removed entry is a standard BitmapDrawable
+
+ 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(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 ImageCache 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 = mCacheParams.diskCacheDir;
+ if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
+ if (!diskCacheDir.exists()) {
+ diskCacheDir.mkdirs();
+ }
+ if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
+ try {
+ mDiskLruCache = DiskLruCache.open(
+ diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Disk cache initialized");
+ }
+ } catch (final IOException e) {
+ mCacheParams.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) {
+ //BEGIN_INCLUDE(add_bitmap_to_cache)
+ if (data == null || value == null) {
+ return;
+ }
+
+ // Add to memory cache
+ if (mMemoryCache != null) {
+ if (RecyclingBitmapDrawable.class.isInstance(value)) {
+ // The removed entry is a recycling drawable, so notify it
+ // that it has been added into the memory cache
+ ((RecyclingBitmapDrawable) value).setIsCached(true);
+ }
+ mMemoryCache.put(data, value);
+ }
+
+ 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(
+ mCacheParams.compressFormat, mCacheParams.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 (BuildConfig.DEBUG && memValue != null) {
+ Log.d(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 (BuildConfig.DEBUG) {
+ Log.d(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 = ImageResizer.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> 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 ImageCache 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 (BuildConfig.DEBUG) {
+ Log.d(TAG, "Memory cache cleared");
+ }
+ }
+
+ synchronized (mDiskCacheLock) {
+ mDiskCacheStarting = true;
+ if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
+ try {
+ mDiskLruCache.delete();
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Disk cache cleared");
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "clearCache - " + e);
+ }
+ mDiskLruCache = null;
+ initDiskCache();
+ }
+ }
+ }
+
+ /**
+ * Flushes the disk cache associated with this ImageCache object. Note that this includes
+ * disk access so this should not be executed on the main/UI thread.
+ */
+ public void flush() {
+ synchronized (mDiskCacheLock) {
+ if (mDiskLruCache != null) {
+ try {
+ mDiskLruCache.flush();
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Disk cache flushed");
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "flush - " + e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Closes the disk cache associated with this ImageCache object. Note that this includes
+ * disk access so this should not be executed on the main/UI thread.
+ */
+ public void close() {
+ synchronized (mDiskCacheLock) {
+ if (mDiskLruCache != null) {
+ try {
+ if (!mDiskLruCache.isClosed()) {
+ mDiskLruCache.close();
+ mDiskLruCache = null;
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Disk cache closed");
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "close - " + e);
+ }
+ }
+ }
+ }
+
+ /**
+ * A holder class that contains cache parameters.
+ */
+ public static class ImageCacheParams {
+ 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;
+ public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
+
+ /**
+ * Create a set of image cache parameters that can be provided to
+ * or
+ * .
+ * @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 ImageCacheParams(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.
+ *
+ * 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 candidate
can be used for inBitmap re-use with
+ * targetOptions
+ */
+ @TargetApi(VERSION_CODES.JELLY_BEAN)
+ private static boolean canUseForInBitmap(
+ Bitmap candidate, BitmapFactory.Options targetOptions) {
+ //BEGIN_INCLUDE(can_use_for_inbitmap)
+ if (!Utils.hasJellyBean()) {
+ // 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.getByteCount();
+ //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.JELLY_BEAN)
+ 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.hasJellyBean()) {
+ return bitmap.getByteCount();
+ }
+
+ 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();
+ }
+
+ /**
+ * Locate an existing instance of this Fragment or if not found, create and
+ * add it using FragmentManager.
+ *
+ * @param fm The FragmentManager manager to use.
+ * @return The existing instance of the Fragment or the new instance if just
+ * created.
+ */
+ private static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
+ //BEGIN_INCLUDE(find_create_retain_fragment)
+ // Check to see if we have retained the worker fragment.
+ RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
+
+ // If not retained (or first time running), we need to create and add it.
+ if (mRetainFragment == null) {
+ mRetainFragment = new RetainFragment();
+ fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
+ }
+
+ return mRetainFragment;
+ //END_INCLUDE(find_create_retain_fragment)
+ }
+
+ /**
+ * A simple non-UI Fragment that stores a single Object and is retained over configuration
+ * changes. It will be used to retain the ImageCache object.
+ */
+ public static class RetainFragment extends Fragment {
+ private Object mObject;
+
+ /**
+ * Empty constructor as per the Fragment documentation
+ */
+ public RetainFragment() {}
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Make sure this Fragment is retained over a configuration change
+ setRetainInstance(true);
+ }
+
+ /**
+ * Store a single object in this Fragment.
+ *
+ * @param object The object to store
+ */
+ public void setObject(Object object) {
+ mObject = object;
+ }
+
+ /**
+ * Get the stored object.
+ *
+ * @return The stored object
+ */
+ public Object getObject() {
+ return mObject;
+ }
+ }
+
+}
diff --git a/opensrp-sdidtk/src/main/java/util/ImageFetcher.java b/opensrp-sdidtk/src/main/java/util/ImageFetcher.java
new file mode 100644
index 000000000..f84a878cb
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/util/ImageFetcher.java
@@ -0,0 +1,310 @@
+/*
+ * 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 util;
+
+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 android.widget.Toast;
+
+import org.ei.opensrp.BuildConfig;
+
+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.URL;
+import java.net.URLConnection;
+
+/**
+ * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL.
+ */
+public class ImageFetcher extends ImageResizer {
+ private static final String TAG = "ImageFetcher";
+ 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;
+
+ /**
+ * Initialize providing a target image width and height for the processing images.
+ *
+ * @param context
+ * @param imageWidth
+ * @param imageHeight
+ */
+ public ImageFetcher(Context context, int imageWidth, int imageHeight) {
+ super(context, imageWidth, imageHeight);
+ init(context);
+ }
+
+ /**
+ * Initialize providing a single target image size (used for both width and height);
+ *
+ * @param context
+ * @param imageSize
+ */
+ public ImageFetcher(Context context, int imageSize) {
+ super(context, imageSize);
+ init(context);
+ }
+
+ private void init(Context context) {
+ checkConnection(context);
+ mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
+ }
+
+ @Override
+ protected void initDiskCacheInternal() {
+ super.initDiskCacheInternal();
+ initHttpDiskCache();
+ }
+
+ private void initHttpDiskCache() {
+ if (!mHttpCacheDir.exists()) {
+ mHttpCacheDir.mkdirs();
+ }
+ synchronized (mHttpDiskCacheLock) {
+ if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) {
+ try {
+ mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE);
+ if (BuildConfig.DEBUG) {
+ Log.d(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 (BuildConfig.DEBUG) {
+ Log.d(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 (BuildConfig.DEBUG) {
+ Log.d(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 (BuildConfig.DEBUG) {
+ Log.d(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()) {
+ Toast.makeText(context, "no network connection", Toast.LENGTH_LONG).show();
+ Log.e(TAG, "checkConnection - no connection found");
+ }
+ }
+
+ /**
+ * The main process method, which will be called by the ImageWorker 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) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "processBitmap - " + data);
+ }
+
+ final String key = ImageCache.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 (BuildConfig.DEBUG) {
+ Log.d(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, mImageWidth,
+ mImageHeight, getImageCache());
+ }
+ if (fileInputStream != null) {
+ try {
+ fileInputStream.close();
+ } catch (IOException e) {}
+ }
+ return bitmap;
+ }
+
+ @Override
+ protected Bitmap processBitmap(Object data) {
+ return processBitmap(String.valueOf(data));
+ }
+
+ /**
+ * 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();
+ URLConnection urlConnection = null;
+ BufferedOutputStream out = null;
+ BufferedInputStream in = null;
+
+ try {
+ final URL url = new URL(urlString);
+ urlConnection = (URLConnection) 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");
+ }
+ }
+}
diff --git a/opensrp-sdidtk/src/main/java/util/ImageResizer.java b/opensrp-sdidtk/src/main/java/util/ImageResizer.java
new file mode 100644
index 000000000..7844b2e5f
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/util/ImageResizer.java
@@ -0,0 +1,270 @@
+/*
+ * 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 util;
+
+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 android.util.Log;
+
+import org.ei.opensrp.BuildConfig;
+
+import java.io.FileDescriptor;
+
+/**
+ * A simple subclass of {@link ImageWorker} that resizes images from resources given a target width
+ * and height. Useful for when the input images might be too large to simply load directly into
+ * memory.
+ */
+public class ImageResizer extends ImageWorker {
+ private static final String TAG = "ImageResizer";
+ protected int mImageWidth;
+ protected int mImageHeight;
+
+ /**
+ * Initialize providing a single target image size (used for both width and height);
+ *
+ * @param context
+ * @param imageWidth
+ * @param imageHeight
+ */
+ public ImageResizer(Context context, int imageWidth, int imageHeight) {
+ super(context);
+ setImageSize(imageWidth, imageHeight);
+ }
+
+ /**
+ * Initialize providing a single target image size (used for both width and height);
+ *
+ * @param context
+ * @param imageSize
+ */
+ public ImageResizer(Context context, int imageSize) {
+ super(context);
+ setImageSize(imageSize);
+ }
+
+ /**
+ * Set the target image width and height.
+ *
+ * @param width
+ * @param height
+ */
+ public void setImageSize(int width, int height) {
+ mImageWidth = width;
+ mImageHeight = height;
+ }
+
+ /**
+ * Set the target image size (width and height will be the same).
+ *
+ * @param size
+ */
+ public void setImageSize(int size) {
+ setImageSize(size, size);
+ }
+
+ /**
+ * The main processing method. This happens in a background task. In this case we are just
+ * sampling down the bitmap and returning it from a resource.
+ *
+ * @param resId
+ * @return
+ */
+ private Bitmap processBitmap(int resId) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "processBitmap - " + resId);
+ }
+ return decodeSampledBitmapFromResource(mResources, resId, mImageWidth,
+ mImageHeight, getImageCache());
+ }
+
+ @Override
+ protected Bitmap processBitmap(Object data) {
+ return processBitmap(Integer.parseInt(String.valueOf(data)));
+ }
+
+ /**
+ * 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 ImageCache 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, ImageCache 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);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+ // 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 ImageCache 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, ImageCache cache) {
+
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(filename, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+ // 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 ImageCache 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, ImageCache cache) {
+
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+ // 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);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache 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)
+ }
+
+ /**
+ * 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)
+ }
+}
diff --git a/opensrp-sdidtk/src/main/java/util/ImageWorker.java b/opensrp-sdidtk/src/main/java/util/ImageWorker.java
new file mode 100644
index 000000000..c98ac626a
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/util/ImageWorker.java
@@ -0,0 +1,485 @@
+/*
+ * 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 util;
+
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.content.Context;
+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 org.ei.opensrp.BuildConfig;
+
+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 ImageWorker {
+ private static final String TAG = "ImageWorker";
+ private static final int FADE_IN_TIME = 200;
+
+ private ImageCache mImageCache;
+ private ImageCache.ImageCacheParams mImageCacheParams;
+ private Bitmap mLoadingBitmap;
+ private boolean mFadeInBitmap = true;
+ private boolean mExitTasksEarly = false;
+ protected boolean mPauseWork = false;
+ private final Object mPauseWorkLock = new Object();
+
+ 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 ImageWorker(Context context) {
+ mResources = context.getResources();
+ }
+
+ /**
+ * Load an image specified by the data parameter into an ImageView (override
+ * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
+ * disk cache will be used if an {@link ImageCache} has been added using
+ * . 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.
+ */
+ public void loadImage(Object data, ImageView imageView) {
+ if (data == null) {
+ return;
+ }
+
+ BitmapDrawable value = null;
+
+ if (mImageCache != null) {
+ value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
+ }
+
+ if (value != null) {
+ // Bitmap found in memory cache
+ imageView.setImageDrawable(value);
+ } else if (cancelPotentialWork(data, imageView)) {
+ //BEGIN_INCLUDE(execute_background_task)
+ final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
+ 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);
+ //END_INCLUDE(execute_background_task)
+ }
+ }
+
+ /**
+ * 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 ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap
+ * caching.
+ * @param fragmentManager
+ * @param cacheParams The cache parameters to use for the image cache.
+ */
+ public void addImageCache(FragmentManager fragmentManager,
+ ImageCache.ImageCacheParams cacheParams) {
+ mImageCacheParams = cacheParams;
+ mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams);
+ new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
+ }
+
+ /**
+ * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap
+ * caching.
+ * @param activity
+ * @param diskCacheDirectoryName See
+ * {@link ImageCache.ImageCacheParams#ImageCacheParams(Context, String)}.
+ */
+ public void addImageCache(Activity activity, String diskCacheDirectoryName) {
+ mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName);
+ mImageCache = ImageCache.getInstance(activity.getFragmentManager(), mImageCacheParams);
+ new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
+ }
+
+ /**
+ * 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 ImageWorker#loadImage(Object, ImageView)}
+ * @return The processed bitmap
+ */
+ protected abstract Bitmap processBitmap(Object data);
+
+ /**
+ * @return The {@link ImageCache} object currently being used by this ImageWorker.
+ */
+ protected ImageCache getImageCache() {
+ return mImageCache;
+ }
+
+ /**
+ * 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 (BuildConfig.DEBUG) {
+ final Object bitmapData = bitmapWorkerTask.mData;
+ Log.d(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) {
+ //BEGIN_INCLUDE(cancel_potential_work)
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Object bitmapData = bitmapWorkerTask.mData;
+ if (bitmapData == null || !bitmapData.equals(data)) {
+ bitmapWorkerTask.cancel(true);
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
+ }
+ } else {
+ // The same work is already in progress.
+ return false;
+ }
+ }
+ return true;
+ //END_INCLUDE(cancel_potential_work)
+ }
+
+ /**
+ * @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 {
+ private Object mData;
+ private final WeakReference imageViewReference;
+
+ public BitmapWorkerTask(Object data, ImageView imageView) {
+ mData = data;
+ imageViewReference = new WeakReference(imageView);
+ }
+
+ /**
+ * Background processing.
+ */
+ @Override
+ protected BitmapDrawable doInBackground(Void... params) {
+ //BEGIN_INCLUDE(load_bitmap_in_background)
+ if (BuildConfig.DEBUG) {
+ Log.d(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 (mImageCache != null && !isCancelled() && getAttachedImageView() != null
+ && !mExitTasksEarly) {
+ bitmap = mImageCache.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);
+ }
+
+ // 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) {
+ if (Utils.hasHoneycomb()) {
+ // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable
+ drawable = new BitmapDrawable(mResources, bitmap);
+ } else {
+ // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable
+ // which will recycle automagically
+ drawable = new RecyclingBitmapDrawable(mResources, bitmap);
+ }
+
+ if (mImageCache != null) {
+ mImageCache.addBitmapToCache(dataString, drawable);
+ }
+ }
+
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "doInBackground - finished work");
+ }
+
+ return drawable;
+ //END_INCLUDE(load_bitmap_in_background)
+ }
+
+ /**
+ * Once the image is processed, associates it to the imageView
+ */
+ @Override
+ protected void onPostExecute(BitmapDrawable value) {
+ //BEGIN_INCLUDE(complete_background_work)
+ // 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 (BuildConfig.DEBUG) {
+ Log.d(TAG, "onPostExecute - setting bitmap");
+ }
+ setImageDrawable(imageView, value);
+ }
+ //END_INCLUDE(complete_background_work)
+ }
+
+ @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;
+ }
+ }
+
+ /**
+ * 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 bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference =
+ new WeakReference(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(android.R.color.transparent),
+ 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.
+ *
+ * If work is paused, be sure setPauseWork(false) is called again
+ * before your fragment or activity is destroyed (for example during
+ * {@link 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();
+ }
+ }
+ }
+
+ protected class CacheAsyncTask extends AsyncTask {
+
+ @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 (mImageCache != null) {
+ mImageCache.initDiskCache();
+ }
+ }
+
+ protected void clearCacheInternal() {
+ if (mImageCache != null) {
+ mImageCache.clearCache();
+ }
+ }
+
+ protected void flushCacheInternal() {
+ if (mImageCache != null) {
+ mImageCache.flush();
+ }
+ }
+
+ protected void closeCacheInternal() {
+ if (mImageCache != null) {
+ mImageCache.close();
+ mImageCache = null;
+ }
+ }
+
+ public void clearCache() {
+ new CacheAsyncTask().execute(MESSAGE_CLEAR);
+ }
+
+ public void flushCache() {
+ new CacheAsyncTask().execute(MESSAGE_FLUSH);
+ }
+
+ public void closeCache() {
+ new CacheAsyncTask().execute(MESSAGE_CLOSE);
+ }
+}
diff --git a/opensrp-sdidtk/src/main/java/util/RecyclingBitmapDrawable.java b/opensrp-sdidtk/src/main/java/util/RecyclingBitmapDrawable.java
new file mode 100644
index 000000000..acf06c2c9
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/util/RecyclingBitmapDrawable.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package util;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+
+import org.ei.opensrp.BuildConfig;
+
+/**
+ * A BitmapDrawable that keeps track of whether it is being displayed or cached.
+ * When the drawable is no longer being displayed or cached,
+ * {@link Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
+ */
+public class RecyclingBitmapDrawable extends BitmapDrawable {
+
+ static final String TAG = "CountingBitmapDrawable";
+
+ private int mCacheRefCount = 0;
+ private int mDisplayRefCount = 0;
+
+ private boolean mHasBeenDisplayed;
+
+ public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
+ super(res, bitmap);
+ }
+
+ /**
+ * Notify the drawable that the displayed state has changed. Internally a
+ * count is kept so that the drawable knows when it is no longer being
+ * displayed.
+ *
+ * @param isDisplayed - Whether the drawable is being displayed or not
+ */
+ public void setIsDisplayed(boolean isDisplayed) {
+ //BEGIN_INCLUDE(set_is_displayed)
+ synchronized (this) {
+ if (isDisplayed) {
+ mDisplayRefCount++;
+ mHasBeenDisplayed = true;
+ } else {
+ mDisplayRefCount--;
+ }
+ }
+
+ // Check to see if recycle() can be called
+ checkState();
+ //END_INCLUDE(set_is_displayed)
+ }
+
+ /**
+ * Notify the drawable that the cache state has changed. Internally a count
+ * is kept so that the drawable knows when it is no longer being cached.
+ *
+ * @param isCached - Whether the drawable is being cached or not
+ */
+ public void setIsCached(boolean isCached) {
+ //BEGIN_INCLUDE(set_is_cached)
+ synchronized (this) {
+ if (isCached) {
+ mCacheRefCount++;
+ } else {
+ mCacheRefCount--;
+ }
+ }
+
+ // Check to see if recycle() can be called
+ checkState();
+ //END_INCLUDE(set_is_cached)
+ }
+
+ private synchronized void checkState() {
+ //BEGIN_INCLUDE(check_state)
+ // If the drawable cache and display ref counts = 0, and this drawable
+ // has been displayed, then recycle
+ if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
+ && hasValidBitmap()) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "No longer being used or cached so recycling. "
+ + toString());
+ }
+
+ getBitmap().recycle();
+ }
+ //END_INCLUDE(check_state)
+ }
+
+ private synchronized boolean hasValidBitmap() {
+ Bitmap bitmap = getBitmap();
+ return bitmap != null && !bitmap.isRecycled();
+ }
+
+}
diff --git a/opensrp-sdidtk/src/main/java/util/Utils.java b/opensrp-sdidtk/src/main/java/util/Utils.java
new file mode 100644
index 000000000..c1949d585
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/util/Utils.java
@@ -0,0 +1,54 @@
+/*
+ * 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 util;
+
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+
+
+/**
+ * Class containing some static utility methods.
+ */
+public class Utils {
+ private Utils() {};
+
+
+
+
+ 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;
+ }
+
+}
diff --git a/opensrp-sdidtk/src/main/java/widget/CircleFlowIndicator.java b/opensrp-sdidtk/src/main/java/widget/CircleFlowIndicator.java
new file mode 100644
index 000000000..d4bfc57ec
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/widget/CircleFlowIndicator.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2011 Patrik Åkerfeldt
+ *
+ * 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 widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.os.AsyncTask;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+
+import org.ei.opensrp.ddtk.R;
+
+
+/**
+ * A FlowIndicator which draws circles (one for each view).
+ *
+ * Available attributes are:
+ *
+ *
+ * activeColor: Define the color used to draw the active circle (default to white)
+ *
+ *
+ * inactiveColor: Define the color used to draw the inactive circles (default to 0x44FFFFFF)
+ *
+ *
+ * inactiveType: Define how to draw the inactive circles, either stroke or fill (default to stroke)
+ *
+ *
+ * activeType: Define how to draw the active circle, either stroke or fill (default to fill)
+ *
+ *
+ * fadeOut: Define the time (in ms) until the indicator will fade out (default to 0 = never fade out)
+ *
+ *
+ * radius: Define the circle outer radius (default to 4.0)
+ *
+ *
+ * spacing: Define the circle spacing (default to 4.0)
+ *
+ *
+ * snap: If true, the 'active' indicator snaps from one page to the next; otherwise, it moves smoothly.
+ *
+ *
+ */
+public class CircleFlowIndicator extends View implements FlowIndicator,
+ AnimationListener {
+ private static final int STYLE_STROKE = 0;
+ private static final int STYLE_FILL = 1;
+
+ private float mRadius = 4;
+ private float mRadiusInactive = 4;
+ private float mRadiusActive = 4;
+ private float spacing = 4;
+ private int fadeOutTime = 0;
+ private final Paint mPaintInactive = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint mPaintActive = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private ViewFlow viewFlow;
+ private int currentScroll = 0;
+ private int currentPosition = 0;
+ private int flowWidth = 0;
+ private FadeTimer timer;
+ public AnimationListener animationListener = this;
+ private Animation animation;
+ private boolean mCentered = false;
+ private boolean mSnap = false;
+
+ /**
+ * Default constructor
+ *
+ * @param context
+ */
+ public CircleFlowIndicator(Context context) {
+ super(context);
+ initColors(0xFFFFFFFF, 0xFFFFFFFF, STYLE_FILL, STYLE_STROKE);
+ }
+
+ /**
+ * The contructor used with an inflater
+ *
+ * @param context
+ * @param attrs
+ */
+ public CircleFlowIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // Retrieve styles attributs
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.CircleFlowIndicator);
+
+ // Gets the active circle type, defaulting to "fill"
+ int activeType = a.getInt(R.styleable.CircleFlowIndicator_activeType,
+ STYLE_FILL);
+
+ int activeDefaultColor = 0xFFFFFFFF;
+
+ // Get a custom active color if there is one
+ int activeColor = a
+ .getColor(R.styleable.CircleFlowIndicator_activeColor,
+ activeDefaultColor);
+
+ // Gets the inactive circle type, defaulting to "stroke"
+ int inactiveType = a.getInt(
+ R.styleable.CircleFlowIndicator_inactiveType, STYLE_STROKE);
+
+ int inactiveDefaultColor = 0x44FFFFFF;
+ // Get a custom inactive color if there is one
+ int inactiveColor = a.getColor(
+ R.styleable.CircleFlowIndicator_inactiveColor,
+ inactiveDefaultColor);
+
+ // Retrieve the radius
+ mRadius = a.getDimension(R.styleable.CircleFlowIndicator_radius, 4.0f);
+ mRadiusActive = mRadius;
+ mRadiusInactive = mRadius;
+
+ // Retrieve the spacing
+ spacing = a.getDimension(R.styleable.CircleFlowIndicator_spacing, 4.0f);
+ // We want the spacing to be center-to-center
+ spacing += 2 * mRadiusActive;
+
+ // Retrieve the fade out time
+ fadeOutTime = a.getInt(R.styleable.CircleFlowIndicator_fadeOut, 0);
+
+ mCentered = a.getBoolean(R.styleable.CircleFlowIndicator_centered, false);
+
+ mSnap = a.getBoolean(R.styleable.CircleFlowIndicator_snap, false);
+
+ initColors(activeColor, inactiveColor, activeType, inactiveType);
+ }
+
+ private void initColors(int activeColor, int inactiveColor, int activeType,
+ int inactiveType) {
+ // Select the paint type given the type attr
+ switch (inactiveType) {
+ case STYLE_FILL:
+ mPaintInactive.setStyle(Style.FILL);
+ break;
+ default:
+ mPaintInactive.setStyle(Style.STROKE);
+ float strokeWidth = mPaintInactive.getStrokeWidth();
+ if (strokeWidth == 0.0f) {
+ // It draws in "hairline mode", which is 1 px wide.
+ strokeWidth = 1.0f / getResources().getDisplayMetrics().density;
+ }
+ mRadiusInactive -= strokeWidth / 2.0f;
+ }
+ mPaintInactive.setColor(inactiveColor);
+
+ // Select the paint type given the type attr
+ switch (activeType) {
+ case STYLE_STROKE:
+ mPaintActive.setStyle(Style.STROKE);
+ float strokeWidth = mPaintInactive.getStrokeWidth();
+ if (strokeWidth == 0.0f) {
+ // It draws in "hairline mode", which is 1 px wide.
+ strokeWidth = 1.0f / getResources().getDisplayMetrics().density;
+ }
+ mRadiusActive -= strokeWidth / 2.0f;
+ break;
+ default:
+ mPaintActive.setStyle(Style.FILL);
+ }
+ mPaintActive.setColor(activeColor);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.view.View#onDraw(android.graphics.Canvas)
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ int count = 3;
+ if (viewFlow != null) {
+ count = viewFlow.getViewsCount();
+ }
+
+ //this is the amount the first circle should be offset to make the entire thing centered
+ float centeringOffset = 0;
+
+ int leftPadding = getPaddingLeft();
+
+ // Draw stroked circles
+ for (int iLoop = 0; iLoop < count; iLoop++) {
+ canvas.drawCircle(leftPadding + mRadius
+ + (iLoop * spacing) + centeringOffset,
+ getPaddingTop() + mRadius, mRadiusInactive, mPaintInactive);
+ }
+ float cx = 0;
+ if (mSnap) {
+ cx = currentPosition * spacing;
+ } else {
+ if (flowWidth != 0) {
+ // Draw the filled circle according to the current scroll
+ cx = (currentScroll * spacing) / flowWidth;
+ }
+ // else, the flow width hasn't been updated yet. Draw the default position.
+ }
+ canvas.drawCircle(leftPadding + mRadius + cx+centeringOffset, getPaddingTop()
+ + mRadius, mRadiusActive, mPaintActive);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.taptwo.android.widget.ViewFlow.ViewSwitchListener#onSwitched(android
+ * .view.View, int)
+ */
+ @Override
+ public void onSwitched(View view, int position) {
+ currentPosition = position;
+ if (mSnap) {
+ setVisibility(View.VISIBLE);
+ resetTimer();
+ invalidate();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.taptwo.android.widget.FlowIndicator#setViewFlow(org.taptwo.android
+ * .widget.ViewFlow)
+ */
+ @Override
+ public void setViewFlow(ViewFlow view) {
+ resetTimer();
+ viewFlow = view;
+ flowWidth = viewFlow.getChildWidth();
+ invalidate();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.taptwo.android.widget.FlowIndicator#onScrolled(int, int, int,
+ * int)
+ */
+ @Override
+ public void onScrolled(int h, int v, int oldh, int oldv) {
+ currentScroll = h;
+ flowWidth = viewFlow.getChildWidth();
+ if (!mSnap) {
+ setVisibility(View.VISIBLE);
+ resetTimer();
+ invalidate();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.view.View#onMeasure(int, int)
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(measureWidth(widthMeasureSpec),
+ measureHeight(heightMeasureSpec));
+ }
+
+ /**
+ * Determines the width of this view
+ *
+ * @param measureSpec
+ * A measureSpec packed into an int
+ * @return The width of the view, honoring constraints from measureSpec
+ */
+ private int measureWidth(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ // We were told how big to be
+ if (specMode == MeasureSpec.EXACTLY) {
+ result = specSize;
+ }
+ // Calculate the width according the views count
+ else {
+ int count = 3;
+ if (viewFlow != null) {
+ count = viewFlow.getViewsCount();
+ }
+ // Remember that spacing is centre-to-centre
+ result = (int) (getPaddingLeft() + getPaddingRight()
+ + (2 * mRadius) + (count - 1) * spacing);
+ // Respect AT_MOST value if that was what is called for by
+ // measureSpec
+ if (specMode == MeasureSpec.AT_MOST) {
+ result = Math.min(result, specSize);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Determines the height of this view
+ *
+ * @param measureSpec
+ * A measureSpec packed into an int
+ * @return The height of the view, honoring constraints from measureSpec
+ */
+ private int measureHeight(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ // We were told how big to be
+ if (specMode == MeasureSpec.EXACTLY) {
+ result = specSize;
+ }
+ // Measure the height
+ else {
+ result = (int) (2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
+ // Respect AT_MOST value if that was what is called for by
+ // measureSpec
+ if (specMode == MeasureSpec.AT_MOST) {
+ result = Math.min(result, specSize);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Sets the fill color
+ *
+ * @param color
+ * ARGB value for the text
+ */
+ public void setFillColor(int color) {
+ mPaintActive.setColor(color);
+ invalidate();
+ }
+
+ /**
+ * Sets the stroke color
+ *
+ * @param color
+ * ARGB value for the text
+ */
+ public void setStrokeColor(int color) {
+ mPaintInactive.setColor(color);
+ invalidate();
+ }
+
+ /**
+ * Resets the fade out timer to 0. Creating a new one if needed
+ */
+ private void resetTimer() {
+ // Only set the timer if we have a timeout of at least 1 millisecond
+ if (fadeOutTime > 0) {
+ // Check if we need to create a new timer
+ if (timer == null || timer._run == false) {
+ // Create and start a new timer
+ timer = new FadeTimer();
+ timer.execute();
+ } else {
+ // Reset the current tiemr to 0
+ timer.resetTimer();
+ }
+ }
+ }
+
+ /**
+ * Counts from 0 to the fade out time and animates the view away when
+ * reached
+ */
+ private class FadeTimer extends AsyncTask {
+ // The current count
+ private int timer = 0;
+ // If we are inside the timing loop
+ private boolean _run = true;
+
+ public void resetTimer() {
+ timer = 0;
+ }
+
+ @Override
+ protected Void doInBackground(Void... arg0) {
+ while (_run) {
+ try {
+ // Wait for a millisecond
+ Thread.sleep(1);
+ // Increment the timer
+ timer++;
+
+ // Check if we've reached the fade out time
+ if (timer == fadeOutTime) {
+ // Stop running
+ _run = false;
+ }
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ animation = AnimationUtils.loadAnimation(getContext(),
+ android.R.anim.fade_out);
+ animation.setAnimationListener(animationListener);
+ startAnimation(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+}
diff --git a/opensrp-sdidtk/src/main/java/widget/FlowIndicator.java b/opensrp-sdidtk/src/main/java/widget/FlowIndicator.java
new file mode 100644
index 000000000..74b2aa480
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/widget/FlowIndicator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 Patrik Åkerfeldt
+ *
+ * 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 widget;
+
+import widget.ViewFlow.ViewSwitchListener;
+
+/**
+ * An interface which defines the contract between a ViewFlow and a
+ * FlowIndicator.
+ * A FlowIndicator is responsible to show an visual indicator on the total views
+ * number and the current visible view.
+ *
+ */
+public interface FlowIndicator extends ViewSwitchListener {
+
+ /**
+ * Set the current ViewFlow. This method is called by the ViewFlow when the
+ * FlowIndicator is attached to it.
+ *
+ * @param view
+ */
+ public void setViewFlow(ViewFlow view);
+
+ /**
+ * The scroll position has been changed. A FlowIndicator may implement this
+ * method to reflect the current position
+ *
+ * @param h
+ * @param v
+ * @param oldh
+ * @param oldv
+ */
+ public void onScrolled(int h, int v, int oldh, int oldv);
+}
diff --git a/opensrp-sdidtk/src/main/java/widget/TitleFlowIndicator.java b/opensrp-sdidtk/src/main/java/widget/TitleFlowIndicator.java
new file mode 100644
index 000000000..f58881f77
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/widget/TitleFlowIndicator.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2011 Patrik Åkerfeldt
+ *
+ * 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 widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+
+import org.ei.opensrp.ddtk.R;
+
+import java.util.ArrayList;
+
+/**
+ * A TitleFlowIndicator is a FlowIndicator which displays the title of left view
+ * (if exist), the title of the current select view (centered) and the title of
+ * the right view (if exist). When the user scrolls the ViewFlow then titles are
+ * also scrolled.
+ *
+ */
+public class TitleFlowIndicator extends TextView implements FlowIndicator {
+
+ private static final float TITLE_PADDING = 10.0f;
+ private static final float CLIP_PADDING = 0.0f;
+ private static final int SELECTED_COLOR = 0xFFFFC445;
+ private static final boolean SELECTED_BOLD = false;
+ private static final int TEXT_COLOR = 0xFFAAAAAA;
+ private static final int TEXT_SIZE = 15;
+ private static final float FOOTER_LINE_HEIGHT = 4.0f;
+ private static final int FOOTER_COLOR = 0xFFFFC445;
+ private static final float FOOTER_TRIANGLE_HEIGHT = 10;
+ private ViewFlow viewFlow;
+ private int currentScroll = 0;
+ private TitleProvider titleProvider = null;
+ private int currentPosition = 0;
+ private Paint paintText;
+ private Paint paintSelected;
+ private Path path;
+ private Paint paintFooterLine;
+ private Paint paintFooterTriangle;
+ private float footerTriangleHeight;
+ private float titlePadding;
+ /**
+ * Left and right side padding for not active view titles.
+ */
+ private float clipPadding;
+ private float footerLineHeight;
+
+ /* These are hardcoded just like in TextView */
+ private static final int SANS = 1;
+ private static final int SERIF = 2;
+ private static final int MONOSPACE = 3;
+
+ private Typeface typeface;
+
+ /**
+ * Default constructor
+ */
+ public TitleFlowIndicator(Context context) {
+ super(context);
+ initDraw(TEXT_COLOR, TEXT_SIZE, SELECTED_COLOR, SELECTED_BOLD, TEXT_SIZE, FOOTER_LINE_HEIGHT, FOOTER_COLOR);
+ }
+
+ /**
+ * The contructor used with an inflater
+ *
+ * @param context
+ * @param attrs
+ */
+ public TitleFlowIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // Retrieve styles attributs
+
+ int typefaceIndex = attrs.getAttributeIntValue("http://schemas.android.com/apk/res/android", "typeface", 0);
+ int textStyleIndex = attrs.getAttributeIntValue("http://schemas.android.com/apk/res/android", "textStyle", 0);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TitleFlowIndicator);
+
+ String customTypeface = a.getString(R.styleable.TitleFlowIndicator_customTypeface);
+ // Retrieve the colors to be used for this view and apply them.
+ int footerColor = a.getColor(R.styleable.TitleFlowIndicator_footerColor, FOOTER_COLOR);
+ footerLineHeight = a.getDimension(R.styleable.TitleFlowIndicator_footerLineHeight, FOOTER_LINE_HEIGHT);
+ footerTriangleHeight = a.getDimension(R.styleable.TitleFlowIndicator_footerTriangleHeight, FOOTER_TRIANGLE_HEIGHT);
+ int selectedColor = a.getColor(R.styleable.TitleFlowIndicator_selectedColor, SELECTED_COLOR);
+ boolean selectedBold = a.getBoolean(R.styleable.TitleFlowIndicator_selectedBold, SELECTED_BOLD);
+ int textColor = a.getColor(R.styleable.TitleFlowIndicator_textColor, TEXT_COLOR);
+ float textSize = a.getDimension(R.styleable.TitleFlowIndicator_textSize, TEXT_SIZE);
+ float selectedSize = a.getDimension(R.styleable.TitleFlowIndicator_selectedSize, textSize);
+ titlePadding = a.getDimension(R.styleable.TitleFlowIndicator_titlePadding, TITLE_PADDING);
+ clipPadding = a.getDimension(R.styleable.TitleFlowIndicator_clipPadding, CLIP_PADDING);
+ initDraw(textColor, textSize, selectedColor, selectedBold, selectedSize, footerLineHeight, footerColor);
+
+ if (customTypeface != null)
+ typeface = Typeface.createFromAsset(context.getAssets(), customTypeface);
+ else
+ typeface = getTypefaceByIndex(typefaceIndex);
+ typeface = Typeface.create(typeface, textStyleIndex);
+
+ }
+
+ /**
+ * Initialize draw objects
+ */
+ private void initDraw(int textColor, float textSize, int selectedColor, boolean selectedBold, float selectedSize, float footerLineHeight, int footerColor) {
+ paintText = new Paint();
+ paintText.setColor(textColor);
+ paintText.setTextSize(textSize);
+ paintText.setAntiAlias(true);
+ paintSelected = new Paint();
+ paintSelected.setColor(selectedColor);
+ paintSelected.setTextSize(selectedSize);
+ paintSelected.setFakeBoldText(selectedBold);
+ paintSelected.setAntiAlias(true);
+ paintFooterLine = new Paint();
+ paintFooterLine.setStyle(Paint.Style.FILL_AND_STROKE);
+ paintFooterLine.setStrokeWidth(footerLineHeight);
+ paintFooterLine.setColor(footerColor);
+ paintFooterTriangle = new Paint();
+ paintFooterTriangle.setStyle(Paint.Style.FILL_AND_STROKE);
+ paintFooterTriangle.setColor(footerColor);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.view.View#onDraw(android.graphics.Canvas)
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Calculate views bounds
+ ArrayList bounds = calculateAllBounds(paintText);
+
+ // If no value then add a fake one
+ int count = (viewFlow != null && viewFlow.getAdapter() != null) ? viewFlow.getAdapter().getCount() : 1;
+
+ // Verify if the current view must be clipped to the screen
+ Rect curViewBound = bounds.get(currentPosition);
+ int curViewWidth = curViewBound.right - curViewBound.left;
+ if (curViewBound.left < 0) {
+ // Try to clip to the screen (left side)
+ clipViewOnTheLeft(curViewBound, curViewWidth);
+ }
+ if (curViewBound.right > getLeft() + getWidth()) {
+ // Try to clip to the screen (right side)
+ clipViewOnTheRight(curViewBound, curViewWidth);
+ }
+
+ // Left views starting from the current position
+ if (currentPosition > 0) {
+ for (int iLoop = currentPosition - 1; iLoop >= 0; iLoop--) {
+ Rect bound = bounds.get(iLoop);
+ int w = bound.right - bound.left;
+ // Si left side is outside the screen
+ if (bound.left < 0) {
+ // Try to clip to the screen (left side)
+ clipViewOnTheLeft(bound, w);
+ // Except if there's an intersection with the right view
+ if (iLoop < count - 1 && currentPosition != iLoop) {
+ Rect rightBound = bounds.get(iLoop + 1);
+ // Intersection
+ if (bound.right + TITLE_PADDING > rightBound.left) {
+ bound.left = rightBound.left - (w + (int) titlePadding);
+ }
+ }
+ }
+ }
+ }
+ // Right views starting from the current position
+ if (currentPosition < count - 1) {
+ for (int iLoop = currentPosition + 1; iLoop < count; iLoop++) {
+ Rect bound = bounds.get(iLoop);
+ int w = bound.right - bound.left;
+ // If right side is outside the screen
+ if (bound.right > getLeft() + getWidth()) {
+ // Try to clip to the screen (right side)
+ clipViewOnTheRight(bound, w);
+ // Except if there's an intersection with the left view
+ if (iLoop > 0 && currentPosition != iLoop) {
+ Rect leftBound = bounds.get(iLoop - 1);
+ // Intersection
+ if (bound.left - TITLE_PADDING < leftBound.right) {
+ bound.left = leftBound.right + (int) titlePadding;
+ }
+ }
+ }
+ }
+ }
+
+ // Now draw views
+ for (int iLoop = 0; iLoop < count; iLoop++) {
+ // Get the title
+ String title = getTitle(iLoop);
+ Rect bound = bounds.get(iLoop);
+ // Only if one side is visible
+ if ((bound.left > getLeft() && bound.left < getLeft() + getWidth()) || (bound.right > getLeft() && bound.right < getLeft() + getWidth())) {
+ Paint paint = paintText;
+ // Change the color is the title is closed to the center
+ int middle = (bound.left + bound.right) / 2;
+ if (Math.abs(middle - (getWidth() / 2)) < 20) {
+ paint = paintSelected;
+ }
+ paint.setTypeface(typeface);
+ canvas.drawText(title, bound.left, bound.bottom, paint);
+ }
+ }
+
+ // Draw the footer line
+ path = new Path();
+ int coordY = getHeight() - 1;
+ coordY -= (footerLineHeight % 2 == 1) ? footerLineHeight / 2 : footerLineHeight / 2 - 1;
+ path.moveTo(0, coordY);
+ path.lineTo(getWidth(), coordY);
+ path.close();
+ canvas.drawPath(path, paintFooterLine);
+ // Draw the footer triangle
+ path = new Path();
+ path.moveTo(getWidth() / 2, getHeight() - footerLineHeight - footerTriangleHeight);
+ path.lineTo(getWidth() / 2 + footerTriangleHeight, getHeight() - footerLineHeight);
+ path.lineTo(getWidth() / 2 - footerTriangleHeight, getHeight() - footerLineHeight);
+ path.close();
+ canvas.drawPath(path, paintFooterTriangle);
+
+ }
+
+ /**
+ * Set bounds for the right textView including clip padding.
+ *
+ * @param curViewBound
+ * current bounds.
+ * @param curViewWidth
+ * width of the view.
+ */
+ private void clipViewOnTheRight(Rect curViewBound, int curViewWidth) {
+ curViewBound.right = getLeft() + getWidth() - (int) clipPadding;
+ curViewBound.left = curViewBound.right - curViewWidth;
+ }
+
+ /**
+ * Set bounds for the left textView including clip padding.
+ *
+ * @param curViewBound
+ * current bounds.
+ * @param curViewWidth
+ * width of the view.
+ */
+ private void clipViewOnTheLeft(Rect curViewBound, int curViewWidth) {
+ curViewBound.left = 0 + (int) clipPadding;
+ curViewBound.right = curViewWidth;
+ }
+
+ /**
+ * Calculate views bounds and scroll them according to the current index
+ *
+ * @param paint
+ * @param currentIndex
+ * @return
+ */
+ private ArrayList calculateAllBounds(Paint paint) {
+ ArrayList list = new ArrayList();
+ // For each views (If no values then add a fake one)
+ int count = (viewFlow != null && viewFlow.getAdapter() != null) ? viewFlow.getAdapter().getCount() : 1;
+ for (int iLoop = 0; iLoop < count; iLoop++) {
+ Rect bounds = calcBounds(iLoop, paint);
+ int w = (bounds.right - bounds.left);
+ int h = (bounds.bottom - bounds.top);
+ bounds.left = (getWidth() / 2) - (w / 2) - currentScroll + (iLoop * getWidth());
+ bounds.right = bounds.left + w;
+ bounds.top = 0;
+ bounds.bottom = h;
+ list.add(bounds);
+ }
+
+ return list;
+ }
+
+ /**
+ * Calculate the bounds for a view's title
+ *
+ * @param index
+ * @param paint
+ * @return
+ */
+ private Rect calcBounds(int index, Paint paint) {
+ // Get the title
+ String title = getTitle(index);
+ // Calculate the text bounds
+ Rect bounds = new Rect();
+ bounds.right = (int) paint.measureText(title);
+ bounds.bottom = (int) (paint.descent() - paint.ascent());
+ return bounds;
+ }
+
+ /**
+ * Returns the title
+ *
+ * @param pos
+ * @return
+ */
+ private String getTitle(int pos) {
+ // Set the default title
+ String title = "title " + pos;
+ // If the TitleProvider exist
+ if (titleProvider != null) {
+ title = titleProvider.getTitle(pos);
+ }
+ return title;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.taptwo.android.widget.FlowIndicator#onScrolled(int, int, int,
+ * int)
+ */
+ @Override
+ public void onScrolled(int h, int v, int oldh, int oldv) {
+ currentScroll = h;
+ invalidate();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.taptwo.android.widget.ViewFlow.ViewSwitchListener#onSwitched(android
+ * .view.View, int)
+ */
+ @Override
+ public void onSwitched(View view, int position) {
+ currentPosition = position;
+ invalidate();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.taptwo.android.widget.FlowIndicator#setViewFlow(org.taptwo.android
+ * .widget.ViewFlow)
+ */
+ @Override
+ public void setViewFlow(ViewFlow view) {
+ viewFlow = view;
+ currentPosition = view.getSelectedItemPosition();
+ invalidate();
+ }
+
+ /**
+ * Set the title provider
+ *
+ * @param provider
+ */
+ public void setTitleProvider(TitleProvider provider) {
+ titleProvider = provider;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.view.View#onMeasure(int, int)
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
+ }
+
+ /**
+ * Determines the width of this view
+ *
+ * @param measureSpec
+ * A measureSpec packed into an int
+ * @return The width of the view, honoring constraints from measureSpec
+ */
+ private int measureWidth(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ if (specMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("ViewFlow can only be used in EXACTLY mode.");
+ }
+ result = specSize;
+ return result;
+ }
+
+ /**
+ * Determines the height of this view
+ *
+ * @param measureSpec
+ * A measureSpec packed into an int
+ * @return The height of the view, honoring constraints from measureSpec
+ */
+ private int measureHeight(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ // We were told how big to be
+ if (specMode == MeasureSpec.EXACTLY) {
+ result = specSize;
+ }
+ // Measure the height
+ else {
+ // Calculate the text bounds
+ Rect bounds = new Rect();
+ bounds.bottom = (int) (paintText.descent() - paintText.ascent());
+ result = bounds.bottom - bounds.top + (int) footerTriangleHeight + (int) footerLineHeight + 10;
+ return result;
+ }
+ return result;
+ }
+
+ private Typeface getTypefaceByIndex(int typefaceIndex) {
+ switch (typefaceIndex) {
+ case SANS:
+ return Typeface.SANS_SERIF;
+
+ case SERIF:
+ return Typeface.SERIF;
+
+ case MONOSPACE:
+ return Typeface.MONOSPACE;
+ default:
+ return Typeface.DEFAULT;
+ }
+ }
+}
diff --git a/opensrp-sdidtk/src/main/java/widget/TitleProvider.java b/opensrp-sdidtk/src/main/java/widget/TitleProvider.java
new file mode 100644
index 000000000..681c04ebb
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/widget/TitleProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 Patrik Åkerfeldt
+ *
+ * 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 widget;
+
+/**
+ * A TitleProvider provides the title to display according to a view.
+ */
+public interface TitleProvider {
+
+ /**
+ * Returns the title of the view at position
+ * @param position
+ * @return
+ */
+ public String getTitle(int position);
+
+}
diff --git a/opensrp-sdidtk/src/main/java/widget/ViewFlow.java b/opensrp-sdidtk/src/main/java/widget/ViewFlow.java
new file mode 100644
index 000000000..3709adf8c
--- /dev/null
+++ b/opensrp-sdidtk/src/main/java/widget/ViewFlow.java
@@ -0,0 +1,841 @@
+/*
+ * Copyright (C) 2011 Patrik Åkerfeldt
+ *
+ * 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 widget;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.Scroller;
+
+
+import org.ei.opensrp.ddtk.R;
+
+import java.util.EnumSet;
+import java.util.LinkedList;
+
+/**
+ * A horizontally scrollable {@link ViewGroup} with items populated from an
+ * {@link Adapter}. The ViewFlow uses a buffer to store loaded {@link View}s in.
+ * The default size of the buffer is 3 elements on both sides of the currently
+ * visible {@link View}, making up a total buffer size of 3 * 2 + 1 = 7. The
+ * buffer size can be changed using the {@code sidebuffer} xml attribute.
+ *
+ */
+public class ViewFlow extends AdapterView {
+
+ private static final int SNAP_VELOCITY = 1000;
+ private static final int INVALID_SCREEN = -1;
+ private final static int TOUCH_STATE_REST = 0;
+ private final static int TOUCH_STATE_SCROLLING = 1;
+
+ private LinkedList mLoadedViews;
+ private LinkedList mRecycledViews;
+ private int mCurrentBufferIndex;
+ private int mCurrentAdapterIndex;
+ private int mSideBuffer = 2;
+ private Scroller mScroller;
+ private VelocityTracker mVelocityTracker;
+ private int mTouchState = TOUCH_STATE_REST;
+ private float mLastMotionX;
+ private int mTouchSlop;
+ private int mMaximumVelocity;
+ private int mCurrentScreen;
+ private int mNextScreen = INVALID_SCREEN;
+ private boolean mFirstLayout = true;
+ private ViewSwitchListener mViewSwitchListener;
+ private ViewLazyInitializeListener mViewInitializeListener;
+ private EnumSet mLazyInit = EnumSet.allOf(LazyInit.class);
+ private Adapter mAdapter;
+ private int mLastScrollDirection;
+ private AdapterDataSetObserver mDataSetObserver;
+ private FlowIndicator mIndicator;
+ private int mLastOrientation = -1;
+ /** Extra return value from obtainView: tells you whether the item it returned on the last call was recycled rather than created by the adapter.
+ * This is a member because getting a second return value requires an allocation. */
+ private boolean mLastObtainedViewWasRecycled = false;
+
+ private OnGlobalLayoutListener orientationChangeListener = new OnGlobalLayoutListener() {
+
+ @Override
+ public void onGlobalLayout() {
+ getViewTreeObserver().removeGlobalOnLayoutListener(
+ orientationChangeListener);
+ setSelection(mCurrentAdapterIndex);
+ }
+ };
+
+ /**
+ * Receives call backs when a new {@link View} has been scrolled to.
+ */
+ public static interface ViewSwitchListener {
+
+ /**
+ * This method is called when a new View has been scrolled to.
+ *
+ * @param view
+ * the {@link View} currently in focus.
+ * @param position
+ * The position in the adapter of the {@link View} currently in focus.
+ */
+ void onSwitched(View view, int position);
+
+ }
+
+ public static interface ViewLazyInitializeListener {
+ void onViewLazyInitialize(View view, int position);
+ }
+
+ enum LazyInit {
+ LEFT, RIGHT
+ }
+
+ public ViewFlow(Context context) {
+ super(context);
+ mSideBuffer = 3;
+ init();
+ }
+
+ public ViewFlow(Context context, int sideBuffer) {
+ super(context);
+ mSideBuffer = sideBuffer;
+ init();
+ }
+
+ public ViewFlow(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
+ R.styleable.ViewFlow);
+ mSideBuffer = styledAttrs.getInt(R.styleable.ViewFlow_sidebuffer, 3);
+ init();
+ }
+
+ private void init() {
+ mLoadedViews = new LinkedList();
+ mRecycledViews = new LinkedList();
+ mScroller = new Scroller(getContext());
+ final ViewConfiguration configuration = ViewConfiguration
+ .get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (newConfig.orientation != mLastOrientation) {
+ mLastOrientation = newConfig.orientation;
+ getViewTreeObserver().addOnGlobalLayoutListener(orientationChangeListener);
+ }
+ }
+
+ public int getViewsCount() {
+ return mAdapter.getCount();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ int childWidth = 0;
+ int childHeight = 0;
+ int childState = 0;
+
+ final int widthPadding = getWidthPadding();
+ final int heightPadding = getHeightPadding();
+
+ int count = mAdapter == null ? 0 : mAdapter.getCount();
+ if (count > 0) {
+ final View child = obtainView(0);
+ measureChild(child, widthMeasureSpec, heightMeasureSpec);
+ childWidth = child.getMeasuredWidth();
+ childHeight = child.getMeasuredHeight();
+ childState = child.getMeasuredState();
+ mRecycledViews.add(child);
+ }
+
+ switch (widthMode) {
+ case MeasureSpec.UNSPECIFIED:
+ widthSize = childWidth + widthPadding;
+ break;
+ case MeasureSpec.AT_MOST:
+ widthSize = (childWidth + widthPadding) | childState;
+ break;
+ case MeasureSpec.EXACTLY:
+ if (widthSize < childWidth + widthPadding)
+ widthSize |= MEASURED_STATE_TOO_SMALL;
+ break;
+ }
+ switch (heightMode) {
+ case MeasureSpec.UNSPECIFIED:
+ heightSize = childHeight + heightPadding;
+ break;
+ case MeasureSpec.AT_MOST:
+ heightSize = (childHeight + heightPadding) | (childState >> MEASURED_HEIGHT_STATE_SHIFT);
+ break;
+ case MeasureSpec.EXACTLY:
+ if (heightSize < childHeight + heightPadding)
+ heightSize |= MEASURED_STATE_TOO_SMALL;
+ break;
+ }
+
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ heightSize = heightPadding + childHeight;
+ } else {
+ heightSize |= (childState&MEASURED_STATE_MASK);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ private int getWidthPadding() {
+ return getPaddingLeft() + getPaddingRight() + getHorizontalFadingEdgeLength() * 2;
+ }
+
+ public int getChildWidth() {
+ return getWidth() - getWidthPadding();
+ }
+
+ private int getHeightPadding() {
+ return getPaddingTop() + getPaddingBottom();
+ }
+
+ public int getChildHeight() {
+ return getHeight() - getHeightPadding();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ final int count = getChildCount();
+ for (int i = 0; i < count ; ++i) {
+ final View child = getChildAt(i);
+ child.measure(MeasureSpec.makeMeasureSpec(getChildWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(getChildHeight(), MeasureSpec.EXACTLY));
+ }
+
+ if (mFirstLayout) {
+ mScroller.startScroll(0, 0, mCurrentScreen * getChildWidth(), 0, 0);
+ mFirstLayout = false;
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int childLeft = getPaddingLeft() + getHorizontalFadingEdgeLength();
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ child.layout(childLeft, getPaddingTop(), childLeft + childWidth,
+ getPaddingTop() + child.getMeasuredHeight());
+ childLeft += childWidth;
+ }
+ }
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ return 0.0f;
+ }
+
+ @Override
+ protected float getBottomFadingEdgeStrength() {
+ return 0.0f;
+ }
+
+ @Override
+ protected float getLeftFadingEdgeStrength() {
+ // always do the fading edge
+ return 1.0f;
+ }
+
+ @Override
+ protected float getRightFadingEdgeStrength() {
+ // always do the fading edge
+ return 1.0f;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (getChildCount() == 0)
+ return false;
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+ final float x = ev.getX();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ /*
+ * If being flinged and user touches, stop the fling. isFinished
+ * will be false if being flinged.
+ */
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+
+ // Remember where the motion event started
+ mLastMotionX = x;
+
+ mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
+ : TOUCH_STATE_SCROLLING;
+
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final int deltaX = (int) (mLastMotionX - x);
+
+ boolean xMoved = Math.abs(deltaX) > mTouchSlop;
+
+ if (xMoved) {
+ // Scroll if the user moved far enough along the X axis
+ mTouchState = TOUCH_STATE_SCROLLING;
+
+ if (mViewInitializeListener != null)
+ initializeView(deltaX);
+ }
+
+ if (mTouchState == TOUCH_STATE_SCROLLING) {
+ // Scroll to follow the motion event
+
+ mLastMotionX = x;
+
+ final int scrollX = getScrollX();
+ if (deltaX < 0) {
+ if (scrollX > 0) {
+ scrollBy(Math.max(-scrollX, deltaX), 0);
+ }
+ } else if (deltaX > 0) {
+ final int availableToScroll = getChildAt(
+ getChildCount() - 1).getRight()
+ - getPaddingRight() - getHorizontalFadingEdgeLength()
+ - scrollX - getWidth();
+ if (availableToScroll > 0) {
+ scrollBy(Math.min(availableToScroll, deltaX), 0);
+ }
+ }
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (mTouchState == TOUCH_STATE_SCROLLING) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int velocityX = (int) velocityTracker.getXVelocity();
+
+ if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
+ // Fling hard enough to move left
+ snapToScreen(mCurrentScreen - 1);
+ } else if (velocityX < -SNAP_VELOCITY
+ && mCurrentScreen < getChildCount() - 1) {
+ // Fling hard enough to move right
+ snapToScreen(mCurrentScreen + 1);
+ } else {
+ snapToDestination();
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ mTouchState = TOUCH_STATE_REST;
+
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mTouchState = TOUCH_STATE_REST;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (getChildCount() == 0)
+ return false;
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+ final float x = ev.getX();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ /*
+ * If being flinged and user touches, stop the fling. isFinished
+ * will be false if being flinged.
+ */
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+
+ // Remember where the motion event started
+ mLastMotionX = x;
+
+ mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
+ : TOUCH_STATE_SCROLLING;
+
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final int deltaX = (int) (mLastMotionX - x);
+
+ boolean xMoved = Math.abs(deltaX) > mTouchSlop;
+
+ if (xMoved) {
+ // Scroll if the user moved far enough along the X axis
+ mTouchState = TOUCH_STATE_SCROLLING;
+
+ if (mViewInitializeListener != null)
+ initializeView(deltaX);
+ }
+
+ if (mTouchState == TOUCH_STATE_SCROLLING) {
+ // Scroll to follow the motion event
+
+ mLastMotionX = x;
+
+ final int scrollX = getScrollX();
+ if (deltaX < 0) {
+ if (scrollX > 0) {
+ scrollBy(Math.max(-scrollX, deltaX), 0);
+ }
+ } else if (deltaX > 0) {
+ final int availableToScroll = getChildAt(
+ getChildCount() - 1).getRight()
+ - getPaddingRight() - getHorizontalFadingEdgeLength()
+ - scrollX - getChildWidth();
+ if (availableToScroll > 0) {
+ scrollBy(Math.min(availableToScroll, deltaX), 0);
+ }
+ }
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (mTouchState == TOUCH_STATE_SCROLLING) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int velocityX = (int) velocityTracker.getXVelocity();
+
+ if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
+ // Fling hard enough to move left
+ snapToScreen(mCurrentScreen - 1);
+ } else if (velocityX < -SNAP_VELOCITY
+ && mCurrentScreen < getChildCount() - 1) {
+ // Fling hard enough to move right
+ snapToScreen(mCurrentScreen + 1);
+ } else {
+ snapToDestination();
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ mTouchState = TOUCH_STATE_REST;
+
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ snapToDestination();
+ mTouchState = TOUCH_STATE_REST;
+ }
+ return true;
+ }
+
+ private void initializeView(final float direction) {
+ if (direction > 0) {
+ if (mLazyInit.contains(LazyInit.RIGHT)) {
+ mLazyInit.remove(LazyInit.RIGHT);
+ if (mCurrentBufferIndex+1 < mLoadedViews.size())
+ mViewInitializeListener.onViewLazyInitialize(mLoadedViews.get(mCurrentBufferIndex + 1), mCurrentAdapterIndex + 1);
+ }
+ } else {
+ if (mLazyInit.contains(LazyInit.LEFT)) {
+ mLazyInit.remove(LazyInit.LEFT);
+ if (mCurrentBufferIndex > 0)
+ mViewInitializeListener.onViewLazyInitialize(mLoadedViews.get(mCurrentBufferIndex - 1), mCurrentAdapterIndex - 1);
+ }
+ }
+ }
+
+ @Override
+ protected void onScrollChanged(int h, int v, int oldh, int oldv) {
+ super.onScrollChanged(h, v, oldh, oldv);
+ if (mIndicator != null) {
+ /*
+ * The actual horizontal scroll origin does typically not match the
+ * perceived one. Therefore, we need to calculate the perceived
+ * horizontal scroll origin here, since we use a view buffer.
+ */
+ int hPerceived = h + (mCurrentAdapterIndex - mCurrentBufferIndex)
+ * getChildWidth();
+ mIndicator.onScrolled(hPerceived, v, oldh, oldv);
+ }
+ }
+
+ private void snapToDestination() {
+ final int screenWidth = getChildWidth();
+ final int whichScreen = (getScrollX() + (screenWidth / 2))
+ / screenWidth;
+
+ snapToScreen(whichScreen);
+ }
+
+ private void snapToScreen(int whichScreen) {
+ mLastScrollDirection = whichScreen - mCurrentScreen;
+ if (!mScroller.isFinished())
+ return;
+
+ whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
+
+ mNextScreen = whichScreen;
+
+ final int newX = whichScreen * getChildWidth();
+ final int delta = newX - getScrollX();
+ mScroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 2);
+ invalidate();
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
+ postInvalidate();
+ } else if (mNextScreen != INVALID_SCREEN) {
+ mCurrentScreen = Math.max(0,
+ Math.min(mNextScreen, getChildCount() - 1));
+ mNextScreen = INVALID_SCREEN;
+ post(new Runnable() {
+ @Override
+ public void run() {
+ postViewSwitched(mLastScrollDirection);
+ }
+ });
+ }
+ }
+
+ /**
+ * Scroll to the {@link View} in the view buffer specified by the index.
+ *
+ * @param indexInBuffer
+ * Index of the view in the view buffer.
+ */
+ private void setVisibleView(int indexInBuffer, boolean uiThread) {
+ mCurrentScreen = Math.max(0,
+ Math.min(indexInBuffer, getChildCount() - 1));
+ int dx = (mCurrentScreen * getChildWidth()) - mScroller.getCurrX();
+ mScroller.startScroll(mScroller.getCurrX(), mScroller.getCurrY(), dx,
+ 0, 0);
+ if(dx == 0)
+ onScrollChanged(mScroller.getCurrX() + dx, mScroller.getCurrY(), mScroller.getCurrX() + dx, mScroller.getCurrY());
+ if (uiThread)
+ invalidate();
+ else
+ postInvalidate();
+ }
+
+ /**
+ * Set the listener that will receive notifications every time the {code
+ * ViewFlow} scrolls.
+ *
+ * @param l
+ * the scroll listener
+ */
+ public void setOnViewSwitchListener(ViewSwitchListener l) {
+ mViewSwitchListener = l;
+ }
+
+ public void setOnViewLazyInitializeListener(ViewLazyInitializeListener l) {
+ mViewInitializeListener = l;
+ }
+
+ @Override
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+ setAdapter(adapter, 0);
+ }
+
+ public void setAdapter(Adapter adapter, int initialPosition) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ mAdapter = adapter;
+
+ if (mAdapter != null) {
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+
+ }
+ if (mAdapter == null || mAdapter.getCount() == 0)
+ return;
+
+ setSelection(initialPosition);
+ }
+
+ @Override
+ public View getSelectedView() {
+ return (mCurrentBufferIndex < mLoadedViews.size() ? mLoadedViews
+ .get(mCurrentBufferIndex) : null);
+ }
+
+ @Override
+ public int getSelectedItemPosition() {
+ return mCurrentAdapterIndex;
+ }
+
+ /**
+ * Set the FlowIndicator
+ *
+ * @param flowIndicator
+ */
+ public void setFlowIndicator(FlowIndicator flowIndicator) {
+ mIndicator = flowIndicator;
+ mIndicator.setViewFlow(this);
+ }
+
+ protected void recycleViews() {
+ while (!mLoadedViews.isEmpty())
+ recycleView(mLoadedViews.remove());
+ }
+
+ protected void recycleView(View v) {
+ if (v == null)
+ return;
+ mRecycledViews.addFirst(v);
+ detachViewFromParent(v);
+ }
+
+ protected View getRecycledView() {
+ return (mRecycledViews.isEmpty() ? null : mRecycledViews.remove());
+ }
+
+ @Override
+ public void setSelection(int position) {
+ mNextScreen = INVALID_SCREEN;
+ mScroller.forceFinished(true);
+ if (mAdapter == null)
+ return;
+
+ position = Math.max(position, 0);
+ position = Math.min(position, mAdapter.getCount() - 1);
+
+ recycleViews();
+
+ View currentView = makeAndAddView(position, true);
+ mLoadedViews.addLast(currentView);
+
+ if (mViewInitializeListener != null)
+ mViewInitializeListener.onViewLazyInitialize(currentView, position);
+
+ for(int offset = 1; mSideBuffer - offset >= 0; offset++) {
+ int leftIndex = position - offset;
+ int rightIndex = position + offset;
+ if(leftIndex >= 0)
+ mLoadedViews.addFirst(makeAndAddView(leftIndex, false));
+ if(rightIndex < mAdapter.getCount())
+ mLoadedViews.addLast(makeAndAddView(rightIndex, true));
+ }
+
+ mCurrentBufferIndex = mLoadedViews.indexOf(currentView);
+ mCurrentAdapterIndex = position;
+
+ requestLayout();
+ setVisibleView(mCurrentBufferIndex, false);
+ if (mIndicator != null) {
+ mIndicator.onSwitched(currentView, mCurrentAdapterIndex);
+ }
+ if (mViewSwitchListener != null) {
+ mViewSwitchListener.onSwitched(currentView, mCurrentAdapterIndex);
+ }
+ }
+
+ private void resetFocus() {
+ logBuffer();
+ recycleViews();
+ removeAllViewsInLayout();
+ mLazyInit.addAll(EnumSet.allOf(LazyInit.class));
+
+ for (int i = Math.max(0, mCurrentAdapterIndex - mSideBuffer); i < Math
+ .min(mAdapter.getCount(), mCurrentAdapterIndex + mSideBuffer
+ + 1); i++) {
+ mLoadedViews.addLast(makeAndAddView(i, true));
+ if (i == mCurrentAdapterIndex) {
+ mCurrentBufferIndex = mLoadedViews.size() - 1;
+ if (mViewInitializeListener != null)
+ mViewInitializeListener.onViewLazyInitialize(mLoadedViews.getLast(), mCurrentAdapterIndex);
+ }
+ }
+ logBuffer();
+ requestLayout();
+ }
+
+ private void postViewSwitched(int direction) {
+ if (direction == 0)
+ return;
+
+ if (direction > 0) { // to the right
+ mCurrentAdapterIndex++;
+ mCurrentBufferIndex++;
+ mLazyInit.remove(LazyInit.LEFT);
+ mLazyInit.add(LazyInit.RIGHT);
+
+ // Recycle view outside buffer range
+ if (mCurrentAdapterIndex > mSideBuffer) {
+ recycleView(mLoadedViews.removeFirst());
+ mCurrentBufferIndex--;
+ }
+
+ // Add new view to buffer
+ int newBufferIndex = mCurrentAdapterIndex + mSideBuffer;
+ if (newBufferIndex < mAdapter.getCount())
+ mLoadedViews.addLast(makeAndAddView(newBufferIndex, true));
+
+ } else { // to the left
+ mCurrentAdapterIndex--;
+ mCurrentBufferIndex--;
+ mLazyInit.add(LazyInit.LEFT);
+ mLazyInit.remove(LazyInit.RIGHT);
+
+ // Recycle view outside buffer range
+ if (mAdapter.getCount() - 1 - mCurrentAdapterIndex > mSideBuffer) {
+ recycleView(mLoadedViews.removeLast());
+ }
+
+ // Add new view to buffer
+ int newBufferIndex = mCurrentAdapterIndex - mSideBuffer;
+ if (newBufferIndex > -1) {
+ mLoadedViews.addFirst(makeAndAddView(newBufferIndex, false));
+ mCurrentBufferIndex++;
+ }
+
+ }
+
+ requestLayout();
+ setVisibleView(mCurrentBufferIndex, true);
+ if (mIndicator != null) {
+ mIndicator.onSwitched(mLoadedViews.get(mCurrentBufferIndex),
+ mCurrentAdapterIndex);
+ }
+ if (mViewSwitchListener != null) {
+ mViewSwitchListener
+ .onSwitched(mLoadedViews.get(mCurrentBufferIndex),
+ mCurrentAdapterIndex);
+ }
+ logBuffer();
+ }
+
+ @Override
+ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
+ LayoutParams lp = child.getLayoutParams();
+ final int childWidthSpec = getChildMeasureSpec(parentWidthMeasureSpec, getWidthPadding(), lp.width);
+ final int childHeightSpec = getChildMeasureSpec(parentHeightMeasureSpec, getHeightPadding(), lp.height);
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ private View setupChild(View child, boolean addToEnd, boolean recycle) {
+ final LayoutParams lp = child.getLayoutParams();
+ child.measure(MeasureSpec.makeMeasureSpec(getChildWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(getChildHeight(), MeasureSpec.EXACTLY));
+ if (recycle)
+ attachViewToParent(child, (addToEnd ? -1 : 0), lp);
+ else
+ addViewInLayout(child, (addToEnd ? -1 : 0), lp, true);
+ return child;
+ }
+
+ private View makeAndAddView(int position, boolean addToEnd) {
+ View view = obtainView(position);
+ return setupChild(view, addToEnd, mLastObtainedViewWasRecycled);
+ }
+
+ private View obtainView(int position) {
+ View convertView = getRecycledView();
+ View view = mAdapter.getView(position, convertView, this);
+ if(view != convertView && convertView != null)
+ mRecycledViews.add(convertView);
+ mLastObtainedViewWasRecycled = (view == convertView);
+ LayoutParams p = view.getLayoutParams();
+ if (p == null) {
+ p = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+ view.setLayoutParams(p);
+ }
+ return view;
+ }
+
+ class AdapterDataSetObserver extends DataSetObserver {
+
+ @Override
+ public void onChanged() {
+ View v = getChildAt(mCurrentBufferIndex);
+ if (v != null) {
+ for (int index = 0; index < mAdapter.getCount(); index++) {
+ if (v.equals(mAdapter.getItem(index))) {
+ mCurrentAdapterIndex = index;
+ break;
+ }
+ }
+ }
+ resetFocus();
+ }
+
+ @Override
+ public void onInvalidated() {
+ // Not yet implemented!
+ }
+
+ }
+
+ private void logBuffer() {
+
+ Log.d("viewflow", "Size of mLoadedViews: " + mLoadedViews.size() +
+ ", Size of mRecycledViews: " + mRecycledViews.size() +
+ ", X: " + mScroller.getCurrX() + ", Y: " + mScroller.getCurrY());
+ Log.d("viewflow", "IndexInAdapter: " + mCurrentAdapterIndex
+ + ", IndexInBuffer: " + mCurrentBufferIndex);
+ }
+}
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/badge.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/badge.png
new file mode 100644
index 000000000..cdb646cc5
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/badge.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/bg_texture.jpg b/opensrp-sdidtk/src/main/res/drawable-mdpi/bg_texture.jpg
new file mode 100644
index 000000000..1299cee55
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/bg_texture.jpg differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/child_boy_infant.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/child_boy_infant.png
new file mode 100644
index 000000000..5fd9411f3
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/child_boy_infant.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/child_girl_infant.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/child_girl_infant.png
new file mode 100644
index 000000000..75ed5a7fc
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/child_girl_infant.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_bpl.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_bpl.png
new file mode 100644
index 000000000..a28872f14
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_bpl.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hp.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hp.png
new file mode 100644
index 000000000..5d8148834
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hp.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hr.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hr.png
new file mode 100644
index 000000000..affa5a7cf
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hr.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hrp.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hrp.png
new file mode 100644
index 000000000..e237df7bb
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_hrp.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_sc.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_sc.png
new file mode 100644
index 000000000..e1fc853d5
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_sc.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_st.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_st.png
new file mode 100644
index 000000000..b1b22714a
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/flag_st.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/homestacks.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/homestacks.png
new file mode 100644
index 000000000..e5839d834
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/homestacks.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_chevron_right.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_chevron_right.png
new file mode 100644
index 000000000..baafa7c1f
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_chevron_right.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_cross.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_cross.png
new file mode 100644
index 000000000..af58beea0
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_cross.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_down.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_down.png
new file mode 100644
index 000000000..de8f51946
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_down.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_dristhi_logo.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_dristhi_logo.png
new file mode 100644
index 000000000..011c0033d
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_dristhi_logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_dropper.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_dropper.png
new file mode 100644
index 000000000..b4f6b6e11
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_dropper.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_menu_edit.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_menu_edit.png
new file mode 100644
index 000000000..699b89a30
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_menu_edit.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_needle.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_needle.png
new file mode 100644
index 000000000..30653b99b
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_needle.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_no.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_no.png
new file mode 100644
index 000000000..635ada3b2
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_no.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_pencil.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_pencil.png
new file mode 100644
index 000000000..a4e337689
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_pencil.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_plus.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_plus.png
new file mode 100644
index 000000000..6b920754d
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_plus.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_remove.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_remove.png
new file mode 100644
index 000000000..44908af30
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_remove.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_reporting.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_reporting.png
new file mode 100644
index 000000000..9f959069c
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_reporting.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_sort.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_sort.png
new file mode 100644
index 000000000..2d99d452e
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_sort.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_tv.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_tv.png
new file mode 100644
index 000000000..d04858cb6
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_tv.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_village_filter.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_village_filter.png
new file mode 100644
index 000000000..e80349020
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_village_filter.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_yes_large.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_yes_large.png
new file mode 100644
index 000000000..f8f96c3d4
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_yes_large.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_yes_small.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_yes_small.png
new file mode 100644
index 000000000..31e31e493
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/ic_yes_small.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_fp_video.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_fp_video.png
new file mode 100644
index 000000000..a9bcefbf0
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_fp_video.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_plus_add_fp.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_plus_add_fp.png
new file mode 100644
index 000000000..28d66cd90
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_plus_add_fp.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_referral_warning.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_referral_warning.png
new file mode 100644
index 000000000..7345db1e5
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/icon_referral_warning.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/login_logo.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/login_logo.png
new file mode 100644
index 000000000..31c53aa8d
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/login_logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/logo.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/logo.png
new file mode 100644
index 000000000..2b9a0bc8b
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/message_box.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/message_box.png
new file mode 100644
index 000000000..434121ab1
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/message_box.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/register_anc.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_anc.png
new file mode 100644
index 000000000..7a890355f
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_anc.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/register_child.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_child.png
new file mode 100644
index 000000000..e90e1dbef
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_child.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/register_ec.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_ec.png
new file mode 100644
index 000000000..53a7bc849
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_ec.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/register_fp.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_fp.png
new file mode 100644
index 000000000..c18e83098
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_fp.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/register_pnc.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_pnc.png
new file mode 100644
index 000000000..c1379fdb5
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/register_pnc.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_homestacks.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_homestacks.png
new file mode 100644
index 000000000..e5839d834
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_homestacks.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_login_logo.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_login_logo.png
new file mode 100644
index 000000000..111886032
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_login_logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_logo.png b/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_logo.png
new file mode 100644
index 000000000..8fd9466d7
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/sdidtk_logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/drawable-mdpi/separator.gif b/opensrp-sdidtk/src/main/res/drawable-mdpi/separator.gif
new file mode 100644
index 000000000..f176b3c06
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/drawable-mdpi/separator.gif differ
diff --git a/opensrp-sdidtk/src/main/res/layout-land/login.xml b/opensrp-sdidtk/src/main/res/layout-land/login.xml
new file mode 100644
index 000000000..1e90ea4f5
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout-land/login.xml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/layout/child_detail_activity.xml b/opensrp-sdidtk/src/main/res/layout/child_detail_activity.xml
new file mode 100644
index 000000000..18728daff
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/child_detail_activity.xml
@@ -0,0 +1,358 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opensrp-sdidtk/src/main/res/layout/child_detail_profile.xml b/opensrp-sdidtk/src/main/res/layout/child_detail_profile.xml
new file mode 100644
index 000000000..375ea7ba7
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/child_detail_profile.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/layout/childdetail_nav_bar.xml b/opensrp-sdidtk/src/main/res/layout/childdetail_nav_bar.xml
new file mode 100644
index 000000000..101f3c58e
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/childdetail_nav_bar.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/layout/login.xml b/opensrp-sdidtk/src/main/res/layout/login.xml
new file mode 100644
index 000000000..5d0e32f4a
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/login.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client.xml b/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client.xml
new file mode 100644
index 000000000..db04b1599
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client.xml
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client_kpsp.xml b/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client_kpsp.xml
new file mode 100644
index 000000000..01ec0c8fa
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client_kpsp.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client_kpsp_detail.xml b/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client_kpsp_detail.xml
new file mode 100644
index 000000000..a3a989d65
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/smart_register_ddtk_client_kpsp_detail.xml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/layout/smart_registers_ddtk_home.xml b/opensrp-sdidtk/src/main/res/layout/smart_registers_ddtk_home.xml
new file mode 100644
index 000000000..888698904
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/layout/smart_registers_ddtk_home.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opensrp-sdidtk/src/main/res/menu/menu_main.xml b/opensrp-sdidtk/src/main/res/menu/menu_main.xml
new file mode 100644
index 000000000..508a1e42e
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/menu/menu_main.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
diff --git a/opensrp-sdidtk/src/main/res/mipmap-hdpi/ic_launcher.png b/opensrp-sdidtk/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-hdpi/logo.png b/opensrp-sdidtk/src/main/res/mipmap-hdpi/logo.png
new file mode 100644
index 000000000..ac362e913
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-hdpi/logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/child_placeholder.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/child_placeholder.png
new file mode 100644
index 000000000..65f450613
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/child_placeholder.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/flag_hrp.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/flag_hrp.png
new file mode 100644
index 000000000..b01289546
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/flag_hrp.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/flag_vg.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/flag_vg.png
new file mode 100644
index 000000000..d5180ba37
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/flag_vg.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/hhreg.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/hhreg.png
new file mode 100644
index 000000000..ceaf70728
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/hhreg.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/household_profile.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/household_profile.png
new file mode 100644
index 000000000..002ce0d7d
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/household_profile.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/household_profile_thumb.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/household_profile_thumb.png
new file mode 100644
index 000000000..f920b3826
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/household_profile_thumb.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/householdload.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/householdload.png
new file mode 100644
index 000000000..c273652f4
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/householdload.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/ic_launcher.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/icon_event_anc.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/icon_event_anc.png
new file mode 100644
index 000000000..025516c98
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/icon_event_anc.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/icon_event_edd.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/icon_event_edd.png
new file mode 100644
index 000000000..b4618f252
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/icon_event_edd.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/login_logo.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/login_logo.png
new file mode 100644
index 000000000..8e3de0203
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/login_logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/logo.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/logo.png
new file mode 100644
index 000000000..ac362e913
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/register_hh.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/register_hh.png
new file mode 100644
index 000000000..3396346c9
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/register_hh.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/sdidtk_login_logo.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/sdidtk_login_logo.png
new file mode 100644
index 000000000..111886032
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/sdidtk_login_logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/station_icon.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/station_icon.png
new file mode 100644
index 000000000..a4b2856f4
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/station_icon.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutoria1.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutoria1.png
new file mode 100644
index 000000000..7c4e01541
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutoria1.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial1bangla.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial1bangla.png
new file mode 100644
index 000000000..2dcd94aba
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial1bangla.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial2.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial2.png
new file mode 100644
index 000000000..99de387d4
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial2.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial2bangla.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial2bangla.png
new file mode 100644
index 000000000..4d567e69d
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial2bangla.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial3.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial3.png
new file mode 100644
index 000000000..d35972fe3
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorial3.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorialbangla3.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorialbangla3.png
new file mode 100644
index 000000000..3e549f6fc
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/tutorialbangla3.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/warning.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/warning.png
new file mode 100644
index 000000000..c6ec4a89d
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/warning.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/woman_placeholder.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/woman_placeholder.png
new file mode 100644
index 000000000..9b47b5a4a
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/woman_placeholder.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-mdpi/womanimageload.png b/opensrp-sdidtk/src/main/res/mipmap-mdpi/womanimageload.png
new file mode 100644
index 000000000..0a6d9d722
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-mdpi/womanimageload.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-xhdpi/ic_launcher.png b/opensrp-sdidtk/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-xhdpi/info.png b/opensrp-sdidtk/src/main/res/mipmap-xhdpi/info.png
new file mode 100644
index 000000000..d68382909
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-xhdpi/info.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-xhdpi/logo.png b/opensrp-sdidtk/src/main/res/mipmap-xhdpi/logo.png
new file mode 100644
index 000000000..ac362e913
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-xhdpi/logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-xxhdpi/ic_launcher.png b/opensrp-sdidtk/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/opensrp-sdidtk/src/main/res/mipmap-xxhdpi/logo.png b/opensrp-sdidtk/src/main/res/mipmap-xxhdpi/logo.png
new file mode 100644
index 000000000..ac362e913
Binary files /dev/null and b/opensrp-sdidtk/src/main/res/mipmap-xxhdpi/logo.png differ
diff --git a/opensrp-sdidtk/src/main/res/values-id/strings.xml b/opensrp-sdidtk/src/main/res/values-id/strings.xml
new file mode 100644
index 000000000..8ec5875a3
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values-id/strings.xml
@@ -0,0 +1,384 @@
+
+
+ SDIDTK
+ Masukan nama pengguna
+ Masukan password
+ Log In
+ logo
+ spacer
+ Logging in…
+ Harap tunggu
+ Log in gagal
+ Harap periksa username dan sandi
+ Are you sure you want to go back?
+ Confirm form close
+ Ya
+ Tidak
+ Household
+ ELCO
+ FP
+ ANC
+ PNC
+ Child
+ Reporting
+ Register
+ Videos
+
+
+
+ Cannot play video.
+ Please install IEC application to play the videos.
+
+ Unable to play video due to technical error.
+ Confirm Log out
+ Are you sure you want to log out? All the data will be
+ cleared.
+
+ forms unsynced
+
+ "Next >>"
+ "<< Previous"
+ Page {0} of {1}
+
+ NAME
+ EC NO.
+ GPLSA
+ FP METHOD
+ CHILDREN
+ STATUS
+
+
+ All Eligible Couples
+ Search EC Register
+ Search FP Register
+ Search PNC Register
+ Search Child Register
+ Sorted By:
+ , Village:
+ ({0})
+
+ Male\n{0}
+ Female\n{0}
+ No FP
+
+ Delivery:
+ LMP:
+ EDD:
+ ANC
+ PNC
+ FP
+ No. strips:
+ No. Given:
+ G
+ P
+ L
+ S
+ A
+
+ (O/A)
+
+ Name (A to Z)
+ EC Number
+ Date of Delivery
+ SC
+ ST
+ High Priority (HP)
+ HRP
+ High Risk (HR)
+ BPL
+
+ All
+ O/A
+ L/P
+ EC:
+
+ Register ANC
+ Register FP
+ Register Child
+ FP Change
+ Change FP
+ Record ECP
+ Edit EC
+ Close EC
+ 26/03 - 25/04
+ Switch Language
+
+ ANC Visit
+ Hb Test
+ IFA
+ TT
+ Delivery Plan
+ PNC Registration
+ ANC Investigations
+ ANC Close
+
+ Reporting
+ Videos
+
+ Child:
+ Overview
+ Immunization 0-9
+ Immunization 9+
+ ID NO
+ DOB
+ SICK STATUS
+ LAST SERVICE
+ BCG
+ HEP B BIRTH
+ OPV
+ PENTAVALENT
+ MEASLES
+ OPV BOOSTER
+ DPT BOOSTER
+ VITAMIN A
+ Thayi:
+ Mother EC:
+ Age
+ HR
+ Child Immunizations
+ Child Illness
+ Child Close
+ Vitamin A
+ PNC
+ Pentavalent 2
+ BCG
+ Pentavalent 3
+ Pentavalent 1
+ OPV 0
+ OPV 1
+ OPV 2
+ OPV 3
+ DPT Boost. 1
+ DPT Boost. 2
+ OPV Boost
+ Measles
+ Measles Booster
+ Measles B
+ Hep B
+ JE
+ MMR
+ Vitamin A
+ Illness Visit
+
+ Sick Visit
+ Illness Report: %s
+ Date:
+ BCG
+ On
+ Hep B
+ OPV
+ TT
+ IFA
+ Pentavalent
+ Measles
+ OPV Booster
+ DPT Booster
+ Vitamin A
+ Pentav 1
+ Pentav 2
+ Pentav 3
+ DPT B1
+ DPT B2
+ Pentav
+
+ FP:
+ PNC:
+ METHOD
+ SIDE EFFECTS
+ FOLLOW UP/REFILL
+ RISKS
+ All Methods
+ Condom
+ DMPA/Injectable
+ IUCD
+ OCP
+ Female Sterilization
+ Male Sterilization
+ Others
+
+ ECP
+ Traditional Methods
+ LAM
+ Centchroman
+ None - PS
+ None - SS
+
+ All EC
+ High Priority (HP)
+ 2+ Children
+ 1 Child
+ Child:
+ "Due "
+ FP Methods
+ FP Prioritization
+ Add FP
+ FP Videos
+
+ THAYI NO.
+ DELIVERY INFO
+ COMPLICATIONS
+ PP FP
+ CHILD
+ DAYS PP
+ FIRST 7 DAYS
+ PNC VISITS
+
+ Overview
+ PNC Visits
+
+ PNC Visit
+ Postpartum family planning
+ PNC Close
+
+ Referred
+ referral
+ follow-up
+ refill
+ Update
+ Side Effects
+
+ ANC:
+ Hb Test
+ "EDD: "
+ day (s) past due
+ LMP:
+ weeks
+ ANC:
+ BP
+ Wt.
+ Search ANC Register
+ Overview
+ ANC Visits
+ TT
+ Hb/IFA
+ Delivery Plan
+ " g/dl
+ " Tablets
+ ID
+ ANC STATUS
+ RISK FACTORS
+ ANC1
+ ANC2
+ ANC3
+ ANC4
+ Other
+ VISITS
+ TT
+ TT 1
+ TT 2
+ TT Booster
+ IFA
+ HB
+ DELIVERY PLAN
+ Delivery At
+ Transport:
+ Has companion:
+ Asha:
+ Contact:
+ Risks reviewed:
+ EDD
+
+ Date:
+ Place:
+ Type:
+ PP FP
+ Wt: %1$s kg
+ DOB:
+
+ PNC
+ expected
+ actual
+ done
+ missed
+ yellow
+ green
+ red
+ ari
+ sam
+ PNC Visit
+
+ Tanggal:
+ Berat:
+ Tinggi:
+ Ling Kepala:
+ Status:
+ Tes Dengar:
+ Tes Lihat:
+ Tes mental:
+ Tes Autist:
+ Tes GPPH:
+
+
+
+
+
+
+
+
+ Profil
+ Antropometri
+ KPSP
+ Tes Hearing/Visual
+ Tes Mental
+ Test Autist/GPPH
+ No of ELCO
+ Last Visit Date
+ HH Visit Due Date
+ Other Inhabitants
+ HH Inhabitants Details
+ All Household Entries
+
+ Instrumen SDIDTK
+ HH
+
+ Profile;
+ Unique ID
+ LMP
+ PSRF Due Date
+ Filtered By :
+ Alphabetical (HoH Name)
+ HHID - Government
+ HHID - JiVitA
+ Due Status
+ Search SDIDTK
+ No ELCO
+ Has one or more ELCO
+ Search Eligible Couples
+ ELCO Woman Details
+ Summary
+ MWRA Registration Date
+ Last PSF Date
+ All Eligible Couples
+ HHID - Government :
+ HHID - JiVitA :
+ BRID :
+ Husband Name :
+ Age :
+ HHID-JiVitA :
+ HHID-Government :
+ Mauza :
+ Alphabetical (Woman name)
+ NBNF due date
+ ANC reminder Status
+ EDD and GA
+ ANC reminder due
+ Take Picture-NID/BRID
+ Census New Woman Registration Form
+ Pregnancy Surveillance And Registration Form
+ All Pregnant Women
+ History
+ Pregnant Woman Details
+ NBNF form
+ ANC visit 4 form
+ ANC visit 3 Form
+ ANC Visit 2 Form
+ ANC Visit 1 Form
+ ANC Register
+ Please check the credentials
+ login failed. Try later
+ No internet connection. Please ensure data connectivity
+ Please check the form for any error/missing input marked in Red box
+ ok
+
+
+
diff --git a/opensrp-sdidtk/src/main/res/values/attrs.xml b/opensrp-sdidtk/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..d97a92818
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values/attrs.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/values/colors.xml b/opensrp-sdidtk/src/main/res/values/colors.xml
new file mode 100644
index 000000000..7f4447f7e
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values/colors.xml
@@ -0,0 +1,38 @@
+
+
+ #1a93d2
+
+ #d9d9d9
+ #494949
+ #f5f5f5
+ #333333
+ #389cc8
+ #e6e6e6
+ #389cc8
+
+ #44ffffff
+ #44389cc8
+ #bb389cc8
+ #1a1a1a
+ #494949
+
+ #cae2ed
+ #d13f3f
+ #389cc8
+ #25aa4a
+ #fdd835
+
+ #f5f5f5
+
+ #EDCA00
+ #d13f3f
+ #25aa4a
+
+ #43ae23
+ #d13f3f
+ #edca00
+
+ #2b475e
+
+ #4fb45b
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/values/dimens.xml b/opensrp-sdidtk/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..635a309a3
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values/dimens.xml
@@ -0,0 +1,70 @@
+
+
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+ 0dp
+
+ 82dp
+ 1dp
+ 80dp
+ 70dp
+ 5dp
+ 5dp
+ 5dp
+ 30dp
+ 22dp
+ 5dp
+ 30dp
+ 3dp
+ 5dp
+
+ 16sp
+ 19sp
+
+ 24sp
+ 22sp
+
+ 49dp
+ 30dp
+
+ 25dp
+
+ 50dp
+ 120dp
+ 16sp
+
+ 150dp
+ 50dp
+ 200dp
+ 1dp
+ 10dp
+ 2dp
+
+ 22sp
+
+ 50dp
+ 540dp
+ 10dp
+ 3dp
+ 22dp
+ 22dp
+ 45dp
+
+ 15dp
+ 10dp
+ 5dp
+ 25dp
+ 25dp
+
diff --git a/opensrp-sdidtk/src/main/res/values/ids.xml b/opensrp-sdidtk/src/main/res/values/ids.xml
new file mode 100644
index 000000000..6aeb3c6a1
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values/ids.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ - client
+ - textforAncRegister
+ - idforalertstatus
+
diff --git a/opensrp-sdidtk/src/main/res/values/integer.xml b/opensrp-sdidtk/src/main/res/values/integer.xml
new file mode 100644
index 000000000..a123600b3
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values/integer.xml
@@ -0,0 +1,90 @@
+
+
+ 1000
+ 244
+ 75
+ 125
+ 109
+ 160
+ 218
+ 89
+
+ 100
+ 26
+ 14
+ 60
+ 12
+ 15
+ 23
+ 10
+
+ 15
+ 15
+ 15
+ 15
+
+ 15
+ 15
+ 15
+ 15
+
+ 100
+ 24
+ 6
+ 10
+ 60
+ 13
+ 7
+ 13
+ 7
+ 20
+ 30
+ 15
+
+ 100
+ 20
+ 9
+ 13
+ 58
+ 12
+ 12
+ 12
+ 12
+ 10
+ 14
+ 14
+ 14
+ 14
+ 2
+ 20
+ 20
+ 18
+ 15
+ 14
+ 15
+ 14
+ 58
+ 9
+ 9
+ 11
+ 9
+ 9
+ 9
+
+ 100
+ 24
+ 8
+ 17
+ 15
+ 9
+ 16
+ 22
+ 12
+ 12
+ 7
+
+ 7
+ 68
+ 7
+
+
\ No newline at end of file
diff --git a/opensrp-sdidtk/src/main/res/values/strings.xml b/opensrp-sdidtk/src/main/res/values/strings.xml
new file mode 100644
index 000000000..6064b9c99
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values/strings.xml
@@ -0,0 +1,416 @@
+
+
+ SDIDTK
+ Masukan nama pengguna
+ Masukan password
+ Log In
+ logo
+ spacer
+ Logging in…
+ Harap tunggu
+ Log in gagal
+ Harap periksa username dan sandi
+ Apakah anda yakin ingin kembali? Semua data yang telah di entry akan hilang
+ Confirm form close
+ Ya
+ Tidak
+ Household
+ ELCO
+ FP
+ ANC
+ PNC
+ Child
+ Reporting
+ Register
+ Videos
+
+
+
+ Cannot play video.
+ Please install IEC application to play the videos.
+
+ Unable to play video due to technical error.
+ Confirm Log out
+ Are you sure you want to log out? All the data will be
+ cleared.
+
+ forms unsynced
+
+ "Next >>"
+ "<< Previous"
+ Page {0} of {1}
+
+ NAME
+ EC NO.
+ GPLSA
+ FP METHOD
+ CHILDREN
+ STATUS
+
+
+ All Eligible Couples
+ Search EC Register
+ Search FP Register
+ Search PNC Register
+ Search Child Register
+ Sorted By:
+ , Village:
+ ({0})
+
+ Male\n{0}
+ Female\n{0}
+ No FP
+
+ Delivery:
+ LMP:
+ EDD:
+ ANC
+ PNC
+ FP
+ No. strips:
+ No. Given:
+ G
+ P
+ L
+ S
+ A
+
+ (O/A)
+
+ Name (A to Z)
+ EC Number
+ Date of Delivery
+ SC
+ ST
+ High Priority (HP)
+ HRP
+ High Risk (HR)
+ BPL
+
+ All
+ O/A
+ L/P
+ EC:
+
+ Register ANC
+ Register FP
+ Register Child
+ FP Change
+ Change FP
+ Record ECP
+ Edit EC
+ Close EC
+ 26/03 - 25/04
+ Switch Language
+
+ ANC Visit
+ Hb Test
+ IFA
+ TT
+ Delivery Plan
+ PNC Registration
+ ANC Investigations
+ ANC Close
+
+ Reporting
+ Videos
+
+ Child:
+ Overview
+ Immunization 0-9
+ Immunization 9+
+ ID NO
+ DOB
+ SICK STATUS
+ LAST SERVICE
+ BCG
+ HEP B BIRTH
+ OPV
+ PENTAVALENT
+ MEASLES
+ OPV BOOSTER
+ DPT BOOSTER
+ VITAMIN A
+ Thayi:
+ Mother EC:
+ Age
+ HR
+ Child Immunizations
+ Child Illness
+ Child Close
+ Vitamin A
+ PNC
+ Pentavalent 2
+ BCG
+ Pentavalent 3
+ Pentavalent 1
+ OPV 0
+ OPV 1
+ OPV 2
+ OPV 3
+ DPT Boost. 1
+ DPT Boost. 2
+ OPV Boost
+ Measles
+ Measles Booster
+ Measles B
+ Hep B
+ JE
+ MMR
+ Vitamin A
+ Illness Visit
+
+ Sick Visit
+ Illness Report: %s
+ Date:
+ BCG
+ On
+ Hep B
+ OPV
+ TT
+ IFA
+ Pentavalent
+ Measles
+ OPV Booster
+ DPT Booster
+ Vitamin A
+ Pentav 1
+ Pentav 2
+ Pentav 3
+ DPT B1
+ DPT B2
+ Pentav
+
+ FP:
+ PNC:
+ METHOD
+ SIDE EFFECTS
+ FOLLOW UP/REFILL
+ RISKS
+ All Methods
+ Condom
+ DMPA/Injectable
+ IUCD
+ OCP
+ Female Sterilization
+ Male Sterilization
+ Others
+
+ ECP
+ Traditional Methods
+ LAM
+ Centchroman
+ None - PS
+ None - SS
+
+ All EC
+ High Priority (HP)
+ 2+ Children
+ 1 Child
+ Child:
+ "Due "
+ FP Methods
+ FP Prioritization
+ Add FP
+ FP Videos
+
+ THAYI NO.
+ DELIVERY INFO
+ COMPLICATIONS
+ PP FP
+ CHILD
+ DAYS PP
+ FIRST 7 DAYS
+ PNC VISITS
+
+ Overview
+ PNC Visits
+
+ PNC Visit
+ Postpartum family planning
+ PNC Close
+
+ Referred
+ referral
+ follow-up
+ refill
+ Update
+ Side Effects
+
+ ANC:
+ Hb Test
+ "EDD: "
+ day (s) past due
+ LMP:
+ weeks
+ ANC:
+ BP
+ Wt.
+ Search ANC Register
+ Overview
+ ANC Visits
+ TT
+ Hb/IFA
+ Delivery Plan
+ " g/dl
+ " Tablets
+ ID
+ ANC STATUS
+ RISK FACTORS
+ ANC1
+ ANC2
+ ANC3
+ ANC4
+ Other
+ VISITS
+ TT
+ TT 1
+ TT 2
+ TT Booster
+ IFA
+ HB
+ DELIVERY PLAN
+ Delivery At
+ Transport:
+ Has companion:
+ Asha:
+ Contact:
+ Risks reviewed:
+ EDD
+
+ Date:
+ Place:
+ Type:
+ PP FP
+ Wt: %1$s kg
+ DOB:
+
+ PNC
+ expected
+ actual
+ done
+ missed
+ yellow
+ green
+ red
+ ari
+ sam
+ PNC Visit
+
+ Tanggal:
+ Berat:
+ Tinggi:
+ Ling Kepala:
+ Status:
+ Tes Dengar:
+ Tes Lihat:
+ Tes mental:
+ Tes Autist:
+ Tes GPPH:
+
+
+
+
+
+
+
+
+ Profil
+ Antropometri
+ KPSP
+ Tes Hearing/Visual
+ Tes Mental
+ Test Autist/GPPH
+ No of ELCO
+ Last Visit Date
+ HH Visit Due Date
+ Other Inhabitants
+ HH Inhabitants Details
+ All Household Entries
+
+ Instrumen SDIDTK
+ HH
+
+ Profile;
+ Unique ID
+ LMP
+ PSRF Due Date
+ Filtered By :
+ Alphabetical (HoH Name)
+ HHID - Government
+ HHID - JiVitA
+ Due Status
+ Search SDIDTK
+ No ELCO
+ Has one or more ELCO
+ Search Eligible Couples
+ ELCO Woman Details
+ Summary
+ MWRA Registration Date
+ Last PSF Date
+ All Eligible Couples
+ HHID - Government :
+ HHID - JiVitA :
+ BRID :
+ Husband Name :
+ Age :
+ HHID-JiVitA :
+ HHID-Government :
+ Mauza :
+ Alphabetical (Woman name)
+ NBNF due date
+ ANC reminder Status
+ EDD and GA
+ ANC reminder due
+ Take Picture-NID/BRID
+ Census New Woman Registration Form
+ Pregnancy Surveillance And Registration Form
+ All Pregnant Women
+ History
+ Pregnant Woman Details
+ NBNF form
+ ANC visit 4 form
+ ANC visit 3 Form
+ ANC Visit 2 Form
+ ANC Visit 1 Form
+ ANC Register
+ Please check the credentials
+ login failed. Try later
+ No internet connection. Please ensure data connectivity
+ Please check the form for any error/missing input marked in Red box
+ ok
+ Tanggal KPSP 1
+ Status Kembang 1
+ Tanggal KPSP 2
+ Status Kembang 2
+ Nama Anak
+ Nama Ibu
+ Umur
+ Tanggal KPSP 3
+ Status Kembang 3
+ Tanggal KPSP 4
+ Status Kembang 4
+ Tanggal KPSP 5
+ Status Kembang 5
+ Tanggal KPSP 6
+ Status Kembang 6
+ Tanggal Tes Dengar
+ Tes Dengar
+ Tanggal Tes Visual
+ Tes Visual
+ Tanggal Tes Menta
+ Tes Mental
+ Tanggal Test Autis
+ Test Autist
+ Tanggal Tes GGPH
+ Tes GGPH
+ Nama Anak
+ Jenis Kelamin
+ Nama Ibu
+ Umur
+ Berat
+ Tinggi
+ Lingkar Kepala
+
+
+
diff --git a/opensrp-sdidtk/src/main/res/values/styles.xml b/opensrp-sdidtk/src/main/res/values/styles.xml
new file mode 100644
index 000000000..c93d1c303
--- /dev/null
+++ b/opensrp-sdidtk/src/main/res/values/styles.xml
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
index adfcd0f44..2d546d80f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,4 @@
rootProject.name = 'opensrp-app-parent'
-include ':opensrp-app', ':opensrp-drishti', ':opensrp-mcare', ':opensrp-indonesia','opensrp-indonesia', ':opensrp-vaccinator'
+include ':opensrp-app', ':opensrp-drishti', ':opensrp-mcare', ':opensrp-indonesia','opensrp-indonesia', ':opensrp-vaccinator', ':opensrp-sdidtk'
project(':opensrp-app').projectDir = "$rootDir/opensrp-app" as File
\ No newline at end of file