From c715551fbb4636644c7c4969abe25e408ae760b7 Mon Sep 17 00:00:00 2001 From: zhaozihanzzh Date: Mon, 11 Feb 2019 18:28:22 +0800 Subject: [PATCH] Make it compatible with Android 2.3 This will set minSdkVersion to 9 and make it work on GingerBread. --- app/build.gradle | 4 +- .../com/majeur/materialicons/Adapter.java | 3 +- .../materialicons/AsyncDataRetriever.java | 56 +- .../majeur/materialicons/AsyncTaskICS.java | 638 ++++++++++++++++++ .../majeur/materialicons/ExportActivity.java | 79 +-- .../majeur/materialicons/MainActivity.java | 15 +- .../dirchooser/DirectoryChooserFragment.java | 497 ++++++++++++++ .../res/drawable-hdpi/ic_action_create.png | Bin 0 -> 308 bytes .../drawable-hdpi/ic_action_create_light.png | Bin 0 -> 289 bytes .../main/res/drawable-hdpi/navigation_up.png | Bin 0 -> 930 bytes .../res/drawable-hdpi/navigation_up_light.png | Bin 0 -> 576 bytes .../main/res/drawable-ldpi/navigation_up.png | Bin 0 -> 583 bytes .../res/drawable-ldpi/navigation_up_light.png | Bin 0 -> 318 bytes .../res/drawable-mdpi/ic_action_create.png | Bin 0 -> 183 bytes .../drawable-mdpi/ic_action_create_light.png | Bin 0 -> 171 bytes .../main/res/drawable-mdpi/navigation_up.png | Bin 0 -> 507 bytes .../res/drawable-mdpi/navigation_up_light.png | Bin 0 -> 460 bytes .../res/drawable-xhdpi/ic_action_create.png | Bin 0 -> 349 bytes .../drawable-xhdpi/ic_action_create_light.png | Bin 0 -> 645 bytes .../main/res/drawable-xhdpi/navigation_up.png | Bin 0 -> 2471 bytes .../drawable-xhdpi/navigation_up_light.png | Bin 0 -> 763 bytes .../res/drawable-xxhdpi/ic_action_create.png | Bin 0 -> 636 bytes .../ic_action_create_light.png | Bin 0 -> 636 bytes .../main/res/drawable/borderless_button.xml | 14 + .../main/res/layout-v11/activity_export.xml | 193 ++++++ .../main/res/layout-v11/directory_chooser.xml | 121 ++++ app/src/main/res/layout-v11/recycler_item.xml | 55 ++ app/src/main/res/layout/activity_export.xml | 5 +- app/src/main/res/layout/directory_chooser.xml | 101 +++ .../res/layout/directory_chooser_activity.xml | 9 + app/src/main/res/layout/list_item.xml | 11 +- app/src/main/res/layout/recycler_item.xml | 3 +- app/src/main/res/menu/directory_chooser.xml | 10 + app/src/main/res/values-de/strings.xml | 15 + app/src/main/res/values-es/strings.xml | 15 + app/src/main/res/values-ja/strings.xml | 15 + app/src/main/res/values-sk/strings.xml | 15 + app/src/main/res/values-v11/colors.xml | 6 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 15 +- 40 files changed, 1794 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/com/majeur/materialicons/AsyncTaskICS.java create mode 100644 app/src/main/java/net/rdrei/android/dirchooser/DirectoryChooserFragment.java create mode 100644 app/src/main/res/drawable-hdpi/ic_action_create.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_create_light.png create mode 100644 app/src/main/res/drawable-hdpi/navigation_up.png create mode 100644 app/src/main/res/drawable-hdpi/navigation_up_light.png create mode 100644 app/src/main/res/drawable-ldpi/navigation_up.png create mode 100644 app/src/main/res/drawable-ldpi/navigation_up_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_create.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_create_light.png create mode 100644 app/src/main/res/drawable-mdpi/navigation_up.png create mode 100644 app/src/main/res/drawable-mdpi/navigation_up_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_create.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_create_light.png create mode 100644 app/src/main/res/drawable-xhdpi/navigation_up.png create mode 100644 app/src/main/res/drawable-xhdpi/navigation_up_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_create.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_create_light.png create mode 100644 app/src/main/res/drawable/borderless_button.xml create mode 100644 app/src/main/res/layout-v11/activity_export.xml create mode 100644 app/src/main/res/layout-v11/directory_chooser.xml create mode 100644 app/src/main/res/layout-v11/recycler_item.xml create mode 100644 app/src/main/res/layout/directory_chooser.xml create mode 100644 app/src/main/res/layout/directory_chooser_activity.xml create mode 100644 app/src/main/res/menu/directory_chooser.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-sk/strings.xml create mode 100644 app/src/main/res/values-v11/colors.xml diff --git a/app/build.gradle b/app/build.gradle index 25c3721..11f691c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "com.majeur.materialicons" - minSdkVersion 14 + minSdkVersion 9 targetSdkVersion 22 versionCode 3 versionName "1.2" @@ -25,6 +25,4 @@ dependencies { compile 'com.android.support:appcompat-v7:22.2.0' compile 'com.android.support:cardview-v7:22.2.0' compile 'com.android.support:recyclerview-v7:22.2.0' - compile 'net.rdrei.android.dirchooser:library:2.1@aar' - compile 'com.afollestad:material-dialogs:0.7.6.0' } diff --git a/app/src/main/java/com/majeur/materialicons/Adapter.java b/app/src/main/java/com/majeur/materialicons/Adapter.java index 7148dcd..4fb023c 100644 --- a/app/src/main/java/com/majeur/materialicons/Adapter.java +++ b/app/src/main/java/com/majeur/materialicons/Adapter.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import android.os.*; public class Adapter extends RecyclerView.Adapter { @@ -92,7 +93,7 @@ public ViewHolder(View itemView, ItemsClickListener listener) { view = (CardView) itemView; titleView = (TextView) itemView.findViewById(R.id.name); iconView = (SVGView) itemView.findViewById(R.id.icon); - iconView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + if (Build.VERSION.SDK_INT > 10) iconView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); mListener = listener; diff --git a/app/src/main/java/com/majeur/materialicons/AsyncDataRetriever.java b/app/src/main/java/com/majeur/materialicons/AsyncDataRetriever.java index b715bbb..38ab6a6 100644 --- a/app/src/main/java/com/majeur/materialicons/AsyncDataRetriever.java +++ b/app/src/main/java/com/majeur/materialicons/AsyncDataRetriever.java @@ -1,24 +1,13 @@ package com.majeur.materialicons; -import android.content.Context; -import android.content.res.AssetManager; -import android.os.AsyncTask; -import android.util.Log; -import android.widget.Toast; - -import com.afollestad.materialdialogs.MaterialDialog; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import android.app.*; +import android.content.*; +import android.content.res.*; +import android.util.*; +import android.widget.*; +import java.io.*; +import java.util.*; +import org.json.*; /** * This class is used to retrieve icons and keep local copies up to date. @@ -30,7 +19,7 @@ * On the server files are in the same folder, to get download url, we just need the file name, then * we format it with the "folder" url. */ -public class AsyncDataRetriever extends AsyncTask { +public class AsyncDataRetriever extends AsyncTaskICS { private static final String TAG = "DataAsyncTask"; @@ -41,7 +30,7 @@ public class AsyncDataRetriever extends AsyncTaskAsyncTask enables proper and easy use of the UI thread. This class allows to + * perform background operations and publish results on the UI thread without + * having to manipulate threads and/or handlers.

+ * + *

An asynchronous task is defined by a computation that runs on a background thread and + * whose result is published on the UI thread. An asynchronous task is defined by 3 generic + * types, called Params, Progress and Result, + * and 4 steps, called onPreExecute, doInBackground, + * onProgressUpdate and onPostExecute.

+ * + *
+ *

Developer Guides

+ *

For more information about using tasks and threads, read the + * Processes and + * Threads developer guide.

+ *
+ * + *

Usage

+ *

AsyncTask must be subclassed to be used. The subclass will override at least + * one method ({@link #doInBackground}), and most often will override a + * second one ({@link #onPostExecute}.)

+ * + *

Here is an example of subclassing:

+ *
+	 * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
+	 *     protected Long doInBackground(URL... urls) {
+	 *         int count = urls.length;
+	 *         long totalSize = 0;
+	 *         for (int i = 0; i < count; i++) {
+	 *             totalSize += Downloader.downloadFile(urls[i]);
+	 *             publishProgress((int) ((i / (float) count) * 100));
+	 *         }
+	 *         return totalSize;
+	 *     }
+	 *
+	 *     protected void onProgressUpdate(Integer... progress) {
+	 *         setProgressPercent(progress[0]);
+	 *     }
+	 *
+	 *     protected void onPostExecute(Long result) {
+	 *         showDialog("Downloaded " + result + " bytes");
+	 *     }
+	 * }
+	 * 
+ * + *

Once created, a task is executed very simply:

+ *
+	 * new DownloadFilesTask().execute(url1, url2, url3);
+	 * 
+ * + *

AsyncTask's generic types

+ *

The three types used by an asynchronous task are the following:

+ *
    + *
  1. Params, the type of the parameters sent to the task upon + * execution.
  2. + *
  3. Progress, the type of the progress units published during + * the background computation.
  4. + *
  5. Result, the type of the result of the background + * computation.
  6. + *
+ *

Not all types are always used by an asynchronous task. To mark a type as unused, + * simply use the type {@link Void}:

+ *
+	 * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
+	 * 
+ * + *

The 4 steps

+ *

When an asynchronous task is executed, the task goes through 4 steps:

+ *
    + *
  1. {@link #onPreExecute()}, invoked on the UI thread immediately after the task + * is executed. This step is normally used to setup the task, for instance by + * showing a progress bar in the user interface.
  2. + *
  3. {@link #doInBackground}, invoked on the background thread + * immediately after {@link #onPreExecute()} finishes executing. This step is used + * to perform background computation that can take a long time. The parameters + * of the asynchronous task are passed to this step. The result of the computation must + * be returned by this step and will be passed back to the last step. This step + * can also use {@link #publishProgress} to publish one or more units + * of progress. These values are published on the UI thread, in the + * {@link #onProgressUpdate} step.
  4. + *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a + * call to {@link #publishProgress}. The timing of the execution is + * undefined. This method is used to display any form of progress in the user + * interface while the background computation is still executing. For instance, + * it can be used to animate a progress bar or show logs in a text field.
  6. + *
  7. {@link #onPostExecute}, invoked on the UI thread after the background + * computation finishes. The result of the background computation is passed to + * this step as a parameter.
  8. + *
+ * + *

Cancelling a task

+ *

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking + * this method will cause subsequent calls to {@link #isCancelled()} to return true. + * After invoking this method, {@link #onCancelled(Object)}, instead of + * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} + * returns. To ensure that a task is cancelled as quickly as possible, you should always + * check the return value of {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

+ * + *

Threading rules

+ *

There are a few threading rules that must be followed for this class to + * work properly:

+ *
    + *
  • The task instance must be created on the UI thread.
  • + *
  • {@link #execute} must be invoked on the UI thread.
  • + *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, + * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • + *
  • The task can be executed only once (an exception will be thrown if + * a second execution is attempted.)
  • + *
+ * + *

Memory observability

+ *

AsyncTask guarantees that all callback calls are synchronized in such a way that the following + * operations are safe without explicit synchronizations.

+ *
    + *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them + * in {@link #doInBackground}. + *
  • Set member fields in {@link #doInBackground}, and refer to them in + * {@link #onProgressUpdate} and {@link #onPostExecute}. + *
+ */ + public abstract class AsyncTaskICS { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue(10); + + /** + * An {@link Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); + + /** + * An {@link Executor} that executes tasks one at a time in serial + * order. This serialization is global to a particular process. + */ + public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + + private static final InternalHandler sHandler = new InternalHandler(); + + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; + private final WorkerRunnable mWorker; + private final FutureTask mFuture; + + private volatile Status mStatus = Status.PENDING; + + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + + private static class SerialExecutor implements Executor { + final ArrayDeque mTasks = new ArrayDeque(); + Runnable mActive; + + public synchronized void execute(final Runnable r) { + mTasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + THREAD_POOL_EXECUTOR.execute(mActive); + } + } + } + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link AsyncTask#onPostExecute} has finished. + */ + FINISHED, + } + + /** @hide Used to force static handler to be created. */ + public static void init() { + sHandler.getLooper(); + } + + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public AsyncTaskICS() { + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + final Result result = get(); + + postResultIfNotInvoked(result); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } catch (Throwable t) { + throw new RuntimeException("An error occured while executing " + + "doInBackground()", t); + } + } + }; + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskICSResult(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 mFuture.isCancelled(); + } + + /** + *

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) { + 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. After + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this + * back to 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}. + */ + public final AsyncTaskICS 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}. + */ + public final AsyncTaskICS 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. + */ + 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 AsyncTaskICSResult(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) { + AsyncTaskICSResult result = (AsyncTaskICSResult) 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 AsyncTaskICSResult { + final AsyncTaskICS mTask; + final Data[] mData; + + AsyncTaskICSResult(AsyncTaskICS task, Data... data) { + mTask = task; + mData = data; + } + } + } diff --git a/app/src/main/java/com/majeur/materialicons/ExportActivity.java b/app/src/main/java/com/majeur/materialicons/ExportActivity.java index b4e28a3..96a3f79 100644 --- a/app/src/main/java/com/majeur/materialicons/ExportActivity.java +++ b/app/src/main/java/com/majeur/materialicons/ExportActivity.java @@ -1,33 +1,19 @@ package com.majeur.materialicons; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.os.Bundle; -import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.v7.app.ActionBarActivity; -import android.view.Gravity; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.CheckBox; -import android.widget.GridView; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.RadioButton; -import android.widget.TextView; -import android.widget.Toast; - -import com.afollestad.materialdialogs.MaterialDialog; - -import net.rdrei.android.dirchooser.DirectoryChooserFragment; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import android.app.*; +import android.content.res.*; +import android.graphics.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.util.*; +import android.view.*; +import android.widget.*; +import java.io.*; +import java.util.*; +import net.rdrei.android.dirchooser.*; + +import android.support.v7.app.AlertDialog; public class ExportActivity extends ActionBarActivity implements DirectoryChooserFragment.OnFragmentInteractionListener { @@ -37,7 +23,7 @@ public class ExportActivity extends ActionBarActivity implements DirectoryChoose R.id.checkbox_mdpi, R.id.checkbox_hdpi, R.id.checkbox_xhdpi, R.id.checkbox_xxhdpi, R.id.checkbox_xxxhdpi}; - private List mAssetsFiles; + private List mAssetsFiles = new ArrayList<>(); private int mColor = Color.DKGRAY; private View mColorPreview; @@ -50,7 +36,7 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_export); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - mAssetsFiles = Arrays.asList(getIntent().getStringArrayExtra(EXTRA_SELECTED_ITEMS)); + mAssetsFiles.addAll(Arrays.asList(getIntent().getStringArrayExtra(EXTRA_SELECTED_ITEMS))); View colorButton = findViewById(R.id.color_button); colorButton.setOnClickListener(mColorClickListener); @@ -100,6 +86,7 @@ public boolean onItemLongClick(AdapterView parent, View view, int position, l Utils.svgFileNameToLabel(mAssetsFiles.remove(position)) + " " + getString(R.string.removed), Toast.LENGTH_SHORT) .show(); + mListAdapter.notifyDataSetChanged(); return true; } @@ -134,10 +121,10 @@ public View getView(int position, View convertView, ViewGroup parent) { } }); - final MaterialDialog dialog = new MaterialDialog.Builder(ExportActivity.this) - .title(R.string.material_colors) - .customView(gridView, false) - .negativeText(android.R.string.cancel) + final AlertDialog dialog = new AlertDialog.Builder(ExportActivity.this) + .setTitle(R.string.material_colors) + .setView(gridView) + .setNegativeButton(android.R.string.cancel, null) .show(); gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @@ -154,7 +141,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) private View.OnClickListener mPathClickListener = new View.OnClickListener() { @Override public void onClick(View v) { - mDialog.show(getFragmentManager(), null); + mDialog.show(getSupportFragmentManager(), null); } }; @@ -190,10 +177,10 @@ private void startExport() { params.path = mPathTextView.getText().toString(); params.saveType = getSaveType(); - final MaterialDialog dialog = new MaterialDialog.Builder(this) - .content("") - .progress(false, 100, true) - .build(); + final ProgressDialog dialog = new ProgressDialog(this); + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + dialog.setProgress(100); + new AsyncExporter(this, new AsyncExporter.ExportStateCallbacks() { @Override @@ -203,18 +190,18 @@ public void onPreExport() { @Override public void onExportProgressUpdate(AsyncExporter.Progress progress) { - dialog.setMaxProgress(progress.totalProgress); - dialog.setProgress(progress.currentProgress); + // dialog.setMaxProgress(progress.totalProgress); + dialog.setProgress((int)(((float)progress.currentProgress/(float)progress.totalProgress)*100)); dialog.setMessage(progress.currentDensity + "\n" + progress.currentFileName); } @Override public void onPostExport(final File resultDirectory) { dialog.dismiss(); - new MaterialDialog.Builder(ExportActivity.this) - .title(R.string.success) - .content(R.string.icons_exported_correctly) - .negativeText(android.R.string.ok) + new AlertDialog.Builder(ExportActivity.this) + .setTitle(R.string.success) + .setMessage(R.string.icons_exported_correctly) + .setNegativeButton(android.R.string.ok, null) .show(); } }).execute(params); @@ -251,7 +238,7 @@ class Holder { public Holder(View view) { imageView = (ImageView) view.findViewById(R.id.image1); - imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + if (Build.VERSION.SDK_INT > 10) imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); textView = (TextView) view.findViewById(R.id.text1); } } diff --git a/app/src/main/java/com/majeur/materialicons/MainActivity.java b/app/src/main/java/com/majeur/materialicons/MainActivity.java index a877cfa..48ebc19 100644 --- a/app/src/main/java/com/majeur/materialicons/MainActivity.java +++ b/app/src/main/java/com/majeur/materialicons/MainActivity.java @@ -4,15 +4,14 @@ import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.StaggeredGridLayoutManager; -import android.view.ActionMode; +import android.support.v7.view.ActionMode; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; -import com.afollestad.materialdialogs.MaterialDialog; - import java.util.List; +import android.support.v7.app.*; public class MainActivity extends ActionBarActivity implements Adapter.ItemsClickListener, ActionMode.Callback { @@ -105,15 +104,15 @@ public boolean onOptionsItemSelected(MenuItem item) { } private void showInfoDialog() { - new MaterialDialog.Builder(this) - .title(R.string.action_about) - .customView(R.layout.about_dialog_message, true) - .negativeText(android.R.string.cancel) + new AlertDialog.Builder(this) + .setTitle(R.string.action_about) + .setView(R.layout.about_dialog_message) + .setNegativeButton(android.R.string.cancel, null) .show(); } private void startSelectionActionMode() { - mActionMode = startActionMode(this); + mActionMode = startSupportActionMode(this); mActionMode.setTitle(R.string.action_export); } diff --git a/app/src/main/java/net/rdrei/android/dirchooser/DirectoryChooserFragment.java b/app/src/main/java/net/rdrei/android/dirchooser/DirectoryChooserFragment.java new file mode 100644 index 0000000..5f00a79 --- /dev/null +++ b/app/src/main/java/net/rdrei/android/dirchooser/DirectoryChooserFragment.java @@ -0,0 +1,497 @@ +package net.rdrei.android.dirchooser; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Environment; +import android.os.FileObserver; +import android.support.v4.app.DialogFragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.majeur.materialicons.*; + +/** + * Activities that contain this fragment must implement the + * {@link DirectoryChooserFragment.OnFragmentInteractionListener} interface + * to handle interaction events. + * Use the {@link DirectoryChooserFragment#newInstance} factory method to + * create an instance of this fragment. + */ +public class DirectoryChooserFragment extends DialogFragment { + public static final String KEY_CURRENT_DIRECTORY = "CURRENT_DIRECTORY"; + private static final String ARG_NEW_DIRECTORY_NAME = "NEW_DIRECTORY_NAME"; + private static final String ARG_INITIAL_DIRECTORY = "INITIAL_DIRECTORY"; + private static final String TAG = DirectoryChooserFragment.class.getSimpleName(); + private String mNewDirectoryName; + private String mInitialDirectory; + + private OnFragmentInteractionListener mListener; + + private Button mBtnConfirm; + private Button mBtnCancel; + private ImageButton mBtnNavUp; + private ImageButton mBtnCreateFolder; + private TextView mTxtvSelectedFolder; + private ListView mListDirectories; + + private ArrayAdapter mListDirectoriesAdapter; + private ArrayList mFilenames; + /** + * The directory that is currently being shown. + */ + private File mSelectedDir; + private File[] mFilesInDir; + private FileObserver mFileObserver; + + + public DirectoryChooserFragment() { + // Required empty public constructor + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param newDirectoryName Name of the directory to create. + * @param initialDirectory Optional argument to define the path of the directory + * that will be shown first. + * If it is not sent or if path denotes a non readable/writable directory + * or it is not a directory, it defaults to + * {@link android.os.Environment#getExternalStorageDirectory()} + * @return A new instance of fragment DirectoryChooserFragment. + */ + public static DirectoryChooserFragment newInstance( + @NonNull final String newDirectoryName, + @Nullable final String initialDirectory) { + DirectoryChooserFragment fragment = new DirectoryChooserFragment(); + Bundle args = new Bundle(); + args.putString(ARG_NEW_DIRECTORY_NAME, newDirectoryName); + args.putString(ARG_INITIAL_DIRECTORY, initialDirectory); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(KEY_CURRENT_DIRECTORY, mSelectedDir.getAbsolutePath()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() == null) { + throw new IllegalArgumentException( + "You must create DirectoryChooserFragment via newInstance()."); + } else { + mNewDirectoryName = getArguments().getString(ARG_NEW_DIRECTORY_NAME); + mInitialDirectory = getArguments().getString(ARG_INITIAL_DIRECTORY); + } + + if (savedInstanceState != null) { + mInitialDirectory = savedInstanceState.getString(KEY_CURRENT_DIRECTORY); + } + + if (this.getShowsDialog()) { + setStyle(DialogFragment.STYLE_NO_TITLE, 0); + } else { + setHasOptionsMenu(true); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + assert getActivity() != null; + final View view = inflater.inflate(R.layout.directory_chooser, container, false); + + mBtnConfirm = (Button) view.findViewById(R.id.btnConfirm); + mBtnCancel = (Button) view.findViewById(R.id.btnCancel); + mBtnNavUp = (ImageButton) view.findViewById(R.id.btnNavUp); + mBtnCreateFolder = (ImageButton) view.findViewById(R.id.btnCreateFolder); + mTxtvSelectedFolder = (TextView) view.findViewById(R.id.txtvSelectedFolder); + mListDirectories = (ListView) view.findViewById(R.id.directoryList); + + mBtnConfirm.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (isValidFile(mSelectedDir)) { + returnSelectedFolder(); + } + } + }); + + mBtnCancel.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + mListener.onCancelChooser(); + } + }); + + mListDirectories.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView adapter, View view, + int position, long id) { + debug("Selected index: %d", position); + if (mFilesInDir != null && position >= 0 + && position < mFilesInDir.length) { + changeDirectory(mFilesInDir[position]); + } + } + }); + + mBtnNavUp.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + File parent; + if (mSelectedDir != null + && (parent = mSelectedDir.getParentFile()) != null) { + changeDirectory(parent); + } + } + }); + + mBtnCreateFolder.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + openNewFolderDialog(); + } + }); + + if (!getShowsDialog()) { + mBtnCreateFolder.setVisibility(View.GONE); + } + + adjustResourceLightness(); + + mFilenames = new ArrayList<>(); + mListDirectoriesAdapter = new ArrayAdapter<>(getActivity(), + android.R.layout.simple_list_item_1, mFilenames); + mListDirectories.setAdapter(mListDirectoriesAdapter); + + final File initialDir; + if (mInitialDirectory != null && isValidFile(new File(mInitialDirectory))) { + initialDir = new File(mInitialDirectory); + } else { + initialDir = Environment.getExternalStorageDirectory(); + } + + changeDirectory(initialDir); + + return view; + } + + private void adjustResourceLightness() { + // change up button to light version if using dark theme + int color = 0xFFFFFF; + final Resources.Theme theme = getActivity().getTheme(); + + if (theme != null) { + TypedArray backgroundAttributes = theme.obtainStyledAttributes( + new int[]{android.R.attr.colorBackground}); + + if (backgroundAttributes != null) { + color = backgroundAttributes.getColor(0, 0xFFFFFF); + backgroundAttributes.recycle(); + } + } + + // convert to greyscale and check if < 128 + if (color != 0xFFFFFF && 0.21 * Color.red(color) + + 0.72 * Color.green(color) + + 0.07 * Color.blue(color) < 128) { + mBtnNavUp.setImageResource(R.drawable.navigation_up_light); + mBtnCreateFolder.setImageResource(R.drawable.ic_action_create_light); + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mListener = (OnFragmentInteractionListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement OnFragmentInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + @Override + public void onPause() { + super.onPause(); + if (mFileObserver != null) { + mFileObserver.stopWatching(); + } + } + + @Override + public void onResume() { + super.onResume(); + if (mFileObserver != null) { + mFileObserver.startWatching(); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.directory_chooser, menu); + + final MenuItem menuItem = menu.findItem(R.id.new_folder_item); + + if (menuItem == null) { + return; + } + + menuItem.setVisible(isValidFile(mSelectedDir) && mNewDirectoryName != null); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + + if (itemId == R.id.new_folder_item) { + openNewFolderDialog(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * Shows a confirmation dialog that asks the user if he wants to create a + * new folder. + */ + private void openNewFolderDialog() { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.create_folder_label) + .setMessage( + String.format(getString(R.string.create_folder_msg), + mNewDirectoryName)) + .setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + } + }) + .setPositiveButton(R.string.confirm_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + int msg = createFolder(); + Toast t = Toast.makeText( + getActivity(), msg, + Toast.LENGTH_SHORT); + t.show(); + } + }).create().show(); + } + + private void debug(String message, Object... args) { + Log.d(TAG, String.format(message, args)); + } + + /** + * Change the directory that is currently being displayed. + * + * @param dir The file the activity should switch to. This File must be + * non-null and a directory, otherwise the displayed directory + * will not be changed + */ + private void changeDirectory(File dir) { + if (dir == null) { + debug("Could not change folder: dir was null"); + } else if (!dir.isDirectory()) { + debug("Could not change folder: dir is no directory"); + } else { + File[] contents = dir.listFiles(); + if (contents != null) { + int numDirectories = 0; + for (File f : contents) { + if (f.isDirectory()) { + numDirectories++; + } + } + mFilesInDir = new File[numDirectories]; + mFilenames.clear(); + for (int i = 0, counter = 0; i < numDirectories; counter++) { + if (contents[counter].isDirectory()) { + mFilesInDir[i] = contents[counter]; + mFilenames.add(contents[counter].getName()); + i++; + } + } + Arrays.sort(mFilesInDir); + Collections.sort(mFilenames); + mSelectedDir = dir; + mTxtvSelectedFolder.setText(dir.getAbsolutePath()); + mListDirectoriesAdapter.notifyDataSetChanged(); + mFileObserver = createFileObserver(dir.getAbsolutePath()); + mFileObserver.startWatching(); + debug("Changed directory to %s", dir.getAbsolutePath()); + } else { + debug("Could not change folder: contents of dir were null"); + } + } + refreshButtonState(); + } + + /** + * Changes the state of the buttons depending on the currently selected file + * or folder. + */ + private void refreshButtonState() { + final Activity activity = getActivity(); + if (activity != null && mSelectedDir != null) { + mBtnConfirm.setEnabled(isValidFile(mSelectedDir)); + getActivity().supportInvalidateOptionsMenu(); + } + } + + /** + * Refresh the contents of the directory that is currently shown. + */ + private void refreshDirectory() { + if (mSelectedDir != null) { + changeDirectory(mSelectedDir); + } + } + + /** + * Sets up a FileObserver to watch the current directory. + */ + private FileObserver createFileObserver(String path) { + return new FileObserver(path, FileObserver.CREATE | FileObserver.DELETE + | FileObserver.MOVED_FROM | FileObserver.MOVED_TO) { + + @Override + public void onEvent(int event, String path) { + debug("FileObserver received event %d", event); + final Activity activity = getActivity(); + + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + refreshDirectory(); + } + }); + } + } + }; + } + + /** + * Returns the selected folder as a result to the activity the fragment's attached to. The + * selected folder can also be null. + */ + private void returnSelectedFolder() { + if (mSelectedDir != null) { + debug("Returning %s as result", mSelectedDir.getAbsolutePath()); + mListener.onSelectDirectory(mSelectedDir.getAbsolutePath()); + } else { + mListener.onCancelChooser(); + } + + } + + /** + * Creates a new folder in the current directory with the name + * CREATE_DIRECTORY_NAME. + */ + private int createFolder() { + if (mNewDirectoryName != null && mSelectedDir != null + && mSelectedDir.canWrite()) { + File newDir = new File(mSelectedDir, mNewDirectoryName); + if (!newDir.exists()) { + boolean result = newDir.mkdir(); + if (result) { + return R.string.create_folder_success; + } else { + return R.string.create_folder_error; + } + } else { + return R.string.create_folder_error_already_exists; + } + } else if (mSelectedDir != null && !mSelectedDir.canWrite()) { + return R.string.create_folder_error_no_write_access; + } else { + return R.string.create_folder_error; + } + } + + /** + * Returns true if the selected file or directory would be valid selection. + */ + private boolean isValidFile(File file) { + return (file != null && file.isDirectory() && file.canRead() && file + .canWrite()); + } + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ + public interface OnFragmentInteractionListener { + /** + * Triggered when the user successfully selected their destination directory. + */ + public void onSelectDirectory(@NonNull String path); + + /** + * Advices the activity to remove the current fragment. + */ + public void onCancelChooser(); + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_action_create.png b/app/src/main/res/drawable-hdpi/ic_action_create.png new file mode 100644 index 0000000000000000000000000000000000000000..0e4f334745a9cae150eea369df595c46706d7e3e GIT binary patch literal 308 zcmV-40n7f0P)ikii z=RZNfzbl9e3|j$X%wB7K{0a;qEZ+N93$e`*veup`;p>|8%{ljgfDH#wffN=%i21<) zAt1y=B^3sQfDjNWbN~ny8aQqjTJQ6_T!WNShZy5;9-#F?>u2s+G*7%!^F*=&bQ!zQ z3(vVPt-NohdE&lx=C}g7N2nA5%mYjZ$8Dob1Cn7ti0L2@!ex$$3Pwt)QRqTU$Iyk) zg_x+I3rUUY{uh%}aBh6FE|i;)bRPsr9jhIW3e^c!j^g4BL694a4D^_Ld{E^P{pAhix z3Q~b#E09u}Roo#9e2B=ah1g~Yseq#weKaA*PFk+5Hc zqV;DUFgiGH8#NkG3F;qu%{K^>DZ`zmHB600000NkvXXu0mjf$w77| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/navigation_up.png b/app/src/main/res/drawable-hdpi/navigation_up.png new file mode 100644 index 0000000000000000000000000000000000000000..b6de77e0fe02725983ffb32d59b57c7de0e357a0 GIT binary patch literal 930 zcmV;T16}-yP)c@L6^{rku3iQOWKveyBGBkMSs8$LTUPodQM4tX&{H347r$| z1V&42&8&Yxz_hk3LX$$krpCMT_26AlL!Cs*sHwxgr`=)rzIkV!Z+2&ZQl(0j`s%c` zwFLlZwOU@Q)$(TBZk5{Y_D;Lq-dSc2q2xrBb`pC58#1_Tmi^PLPRPgR{%UqlH}L_ z9YO^*-EQ|DfE^LJB$<&U0Ful^Bl$DQ zw}k-f*?9PJj9|#=B|N)*G{L?7m*(TqyQ`cm9M~0N~r|EJpijY z|EW^y7XZHjXy+yhyt<7>?A0Q*pj_=XBr8g(C&m~!&VoQm(Lxq6l#ue>7nn$B3c`u5h~1!1ng`T-c08qfh2`G#br? zQ!$pg5IQ+I!RF@XH_c|V58yiyxynDL|BB75ZbE1>nOqfpj*pM=0i1;7R-@6dYinz- zwbp+vgplWXisT&;spiZtgw4D-0k9%cC3(m5Jhc!)zVE{r^TrtS6u_=hYE?uQ#^zIT z%5$s&*fqvHHO9ONf?$3F#uxy=U@$MI7AqR&|1GF z`N&%PB1w|xgTVj*ptZi4l8*)7+uJK1cWbqpt=H?Hh0TX>s@LnN)oSDO^Ke-_2Yz-ij5tAo-)UcJ@y60;NioD)rUrZ@Nn+$qN`(S^xk507*qoM6N<$ Ef-g9zSO5S3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/navigation_up_light.png b/app/src/main/res/drawable-hdpi/navigation_up_light.png new file mode 100644 index 0000000000000000000000000000000000000000..2ef403854c93b642867ce469302e571c67e10a62 GIT binary patch literal 576 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!pj3%#L`iUdT1k0gQ7S_~VrE{6o}X)oLYc9i zsh**M!Iz?iKx5Z?x;TbpIKRDS>&4YCkrfeSK)%_M8WcGG|>#k?7=6o|vd&)Z@}N@q~epq?MK5 zmATxKR+c97InzypecSl@&g`!Aw3xi|rk2r~D{=n}BcnMb!vapP-`c{XE)`XGaxaj6 zq4z&$-X+O7C+~Ql*bq?tll^m6#235TZRhV8J`QtgW8HOn|4i%RgmpZY7LyB)_W(^u z3p)Kd_@<%svs)i}n~f#Q&+M2wA+c@W)9*arqn{nq;Vw{n+$XtR$VkR;n@t z>`!$1H!>AyDjt^Gd0Kfn!z%9+0UD8VJ6Qulo!Vs8pB;Xp*FT9VK(%7>_N#xoc&yKz z-5zOYu+7GcdqHfI|Gg)KIO%NWe$RpRx`MtOj_8y(?Jgx^M={>XW9>amZhyeapB;yod(l6(~V4{ nT_Hi~rgYc?8on}TABoQlJbGb~<*Ybh1T%QL`njxgN@xNA3*GGg literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-ldpi/navigation_up.png b/app/src/main/res/drawable-ldpi/navigation_up.png new file mode 100644 index 0000000000000000000000000000000000000000..2b77ccabce0f4a0ac30e25357b725adb87167dbc GIT binary patch literal 583 zcmV-N0=WH&P)7PBlvQ50~_9fS~00So{2zi!p{lO|Ac~?-Q51bt z)mN(e0)QQl$DfO$xZA40T05$38IrsvdFH*Z9@QG}eMRz&n|h_x0XQpRySQ`MUg z!j*{JuH7)1Or8KVnty1mJ-61L=XrkER?~*@lY(Ba_mHP)3K7|QQbhz!mECT4w}aMH zP*v62dWzn8@4xNkF8ba0_X*-(Ees(LK(~?am!PVulH`Y)pxmp>$Fu!Mf17_@dQ3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-ldpi/navigation_up_light.png b/app/src/main/res/drawable-ldpi/navigation_up_light.png new file mode 100644 index 0000000000000000000000000000000000000000..1d656b9bfdb575914c4aa8e5991f0e80c85f7788 GIT binary patch literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;>1s;*b z3=DjSK$uZf!>a)(C{^MbQ4*Y=R#Ki=l*$m0n3-3i=jR%tP-d)Ws%L0m@TF)WP}MF^ z7sn6}@3+0)Tug=nF3ikw2U;H|nHrcTKGaLn=+1$Lnt(^4;vf(x-SM^k2eoHqT#3x+1XB;_DR z<9nNguDXB^Yfy=V1zV>VI9APU1HhNNikFI>_6Zj%$O!u)0$?sKQ?tTqu|Uv8&~xi zFfb`FF#T72@X^bqHf2#%_VZn_``Nypw%n#4I44iR=;XqK(O@MC6aMMe3*9ciwQM&7 d+dhR2jBj%a6z7`9-vPRW!PC{xWt~$(696iHKOq1B literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_action_create_light.png b/app/src/main/res/drawable-mdpi/ic_action_create_light.png new file mode 100644 index 0000000000000000000000000000000000000000..a4c84f0f2df15909fd8da44ec09467ec87062d01 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJB2O2`kO=qW6p4fb76-l=_gFP7 z68Q1|e?7Yfm!zbm_K99EFWCa2R}7y6*?Za!WN-k951fn{A76Pa>5o|O>Yu>MI{T`P zy@4Y9JD5AZa7IX8*tod#AXpukIAC_#Q*&G4%B3sf4PG&1wJ*`A<_UZ5NA;1LRej1uWqaOn{& z5o%sHDTq?5BX-k#t0Bqb`vr#l0m}UMknf|3s?R|Nva`qKwtJnvkI=oLU6K)qON0c(rMqjT=4jcI(!jWI)J&WH#^1ZK{R zF+(4O@hK02-~)heU9Le8eE1-YPk9{2doyty!w0bkh@$A^@?#NE6#WiJk^~|$0MI?9 z{15^_S40L$lKc(`!w_kjjsT2Rm6`eI@?8Wo4^@=`jMFq7g<*Kw%c$4u)mE!jHlNS; z&h`8K`#jGd0KgdYGMP-?cdzMmI%qT+wb^Wz{nSx81UNK!ObU2*)`RHMDFG#*1XO$n x^E@{Is$1*71#C8(1ra?LUqp-Y52uW8z5wEX8|sW``o#bM002ovPDHLkV1oFT)!hI9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/navigation_up_light.png b/app/src/main/res/drawable-mdpi/navigation_up_light.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c67204f3f0dc7ceffea37d53e9962919b6810b GIT binary patch literal 460 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!pj3%#L`iUdT1k0gQ7S_~VrE{6o}X)oLYc9i zsh**M!Iz?iKx3^vT^vIq+}~bN^kZ=pX?dusp>ptHfribz1WQ)&1I0WBcN(|_&M|NZ z98gn{NnJ62kEfNf>E*l1RcAZ&D_72|U4JES*Sg!9(%VuFPs_P6iC23L>-VrDZLNGy z6b*LgZ_0^iXfEHxd#2m#hJmu}KPj>48y@z2`7!$o>kRR!8y>d&`>}lwcY8dOv1ERz zYEH(InEWv;^iUh~;m7_(I?pFI&g|K~O32J-$7A(Fw_9si(~Rz|0vdQm zwq3gcC_Y7b=k%42XJ|Dfa+Ym0{`>5{21ocOZJqMthT#mgocC@XJ@?RjGV8aeZu5@H zy02sSp=*8P=(&sLixoDiw>RsA1~I%zm|i_wcS3Mf!Zcp)wXD~evKjk6nRm|>*)e7Q ufj9e3Zc1Djyp#9LyV*K$f8Ks+R=*)H%6E5=DF-mH89ZJ6T-G@yGywpyQCY+jdU5 z6<-`bXf=s{u!wU2()>%=r6SH;*()Vc^76#?>zkJSoV~>J-2h6YBahz7%?m)@@xUq?|2##EXw1Lh*iA ojLw3ovo~0=izT$yXcyhTRQJy@=&VDj;FaSW-5dpmoh5lf&5tFO!?G$b+|etegK^$E+h(!KYBj_aS_rE}W*p4n`R)4VrW zY;td`>$|Wwk0~#`@Y!Y23C7OCTn>fLQn^mdT*%3~z~ZwJ>ytAVT9^XnRL*27O3QF! z)Ht`thtbm5WFdn{dfc*xIg+yi7`lx0gB{NC_-Zg5ojF}oA-zpaq~Xz-(;`$eE{U=I zv>^Wr%}<+7>lW2~EB$>pI<8gi-DPhjd%=RuvOfiFzx~(TeVfHbUEZCMbM58xS`SPX z$IcHpu&%;?`v0zme(A0{&U)SC|N0Lv>mU5*U*TO;f9$?rKm(Hj)5rM>Z#-Faf>+~# zU?8i2NH-s=$cK&9gGp)~k+Q0ZV|KI7K zxFohMWMJhHmXLeTrjan?z=sNR2gbuJZO3;ra!IHe*xciVq=NV2Jvjy6l8p3$$&10$ L)z4*}Q$iB}I{NyA literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/navigation_up.png b/app/src/main/res/drawable-xhdpi/navigation_up.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c3e6f752955a6cedfb5d65bb8d4fcb355b1c45 GIT binary patch literal 2471 zcmah}3pmqzAD@}Axn}NkteJbvRJ0kBODy+mRANLE+n8ZuS<(?I3H7FlD5N7K%AFFT zxpi^N^|*$vNV%P@)9HQRbDrmY|IhRL|3AOa|9km-zQ5=BB|6$$i9!{i002OgU~S>d z{hIQY5SaTe3Je_q06@qsR#mpGZ938SS_71Gbly^IKioajRQC3TAD1k@CChs6zWSiwZ#K2C%_VS*IM#H4S#-@iGt_-Lez>Fj z(6#44m6T%*1)NEiAPfJXxVBJ$(~C9Sl`Hef=3o%z-kp7e_cA@5gg5lB868b&(P;prvO{Ew(t|yMK~M2G>{h8^|MdkSnV1F*QfD!=`x-}*=hOC25l|< zODY8sN7z$Wk{H+#?~@&xTPJn_AKs{4J-Vt^n$N$ynFiCtV#mlkZB^TLO=Mw1 z-7S}(^y}RmuT$Vrv0TF4h}Q<~(gz1hib^ygG3}4^y;k(Y5V!HWS12hdm6|7q>2T3nOJig{^yr3P#?DEtwww&v;KjC?i-=8^*mhAR*SC zVE}+I7YoDq0GVt#001Z#=;}fDu(QSbQOUZ*V^m*%T?RRX8w~*97+5YO`_qYV2054# zhGiHdzDHoWm`9@!@b4k?AY+7wog>_wO7n;B(M9X(BTS%hI2=bi7JzlO*#9G)yE8_R z==2aQ3Kba{sT;XVmr6U1LSrx(l%76HUtfnCp%WHGp%WQ8lrWV)i~Lo`!avN978pVg zq*CC#xur$!pOFIA-j*ymdk2HAT1==!IrZ z6jUVPiQ#XV=t7K-{bF^UGGqjmsHi9m!@02^o84@~&514`N?B zK+U^vSslr|@J;ZQRAot&?I6S#BscYOfw%EF zAO;Ui$NPYk98JtCe~Tzvvt&!ev-sN;Ya80?3BaKRKeS)J*t>W&IljYJRoZ6&k6K@h z!YZ$dmUeiJ8h_iHPq24XrypqCHYJS2pgL$5hrBMV{BoVV@=>mp#R7)MDlPihli~m~ zOElXkZAV@0kasny26v&Aw5Q@_${+dPd%ThuT63*0I<{_fXDzVS%;gazS|C z>pdZPO}4U4q+5GpEn-Lp5^zC zJA%2w?xfm@3!St6#QXr#j=7(TAI?@dJa?;4onDS`%8td?PM&YgZQ>~pn*C>c(yXgR z-+d=MPD@l)-?D{}d-zq%C{5 yYf@sv@(*%9`~{B~8-q-P#X3q-_-rxfFY}rUNkRr&_T~c9CWEJ|pUXO@geCxJB}AtH literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_create.png b/app/src/main/res/drawable-xxhdpi/ic_action_create.png new file mode 100644 index 0000000000000000000000000000000000000000..9322b1366aeb20c3fb4f22539f7a44fd1668b40c GIT binary patch literal 636 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U~=|!aSW-5dpmcd7jvKp>)j<6 z&-rscIs7K)=(ThF+)skvZ1GImBDB$R-ob9GmEY&=KJzA(^U1`ght6v>rk3nFtS@5l zM?k}Y@i0r<@!gDE5^4rE_jm&i7&JconAgD6#>vxP&MY!P_(qP|_4uXVe*X==XEyuC zROtBCmj|OS-{Za@W+;6c8thQ zMphf8RSZo@7Z)}#$q9xsIBt9q;K115q1CV;qC{gsgS(5ULx7HzNWcN*MO+FRVzOKs z4+H~Q1w^{}SVcZ`tYG5ka&2egs&ENmWI4LXnNs8GlU)P0Jf3@C{h!KT@kPrU5@&Ov sN2h^LLd72G1r3QzhacZU~=|!aSW-5dpmcd7jvKp>)j<6 z&-rscIs7K)=(ThF+)skvZ1GImBDB$R-ob9GmEY&=KJzA(^U1`ght6v>rk3nFtS@5l zM?k}Y@i0r<@!gDE5^4rE_jm&i7&JconAgD6#>vxP&MY!P_(qP|_4uXVe*X==XEyuC zROtBCmj|OS-{Za@W+;6c8thQ zMphf8RSZo@7Z)}#$q9xsIBt9q;K115q1CV;qC{gsgS(5ULx7HzNWcN*MO+FRVzOKs z4+H~Q1w^{}SVcZ`tYG5ka&2egs&ENmWI4LXnNs8GlU)P0Jf3@C{h!KT@kPrU5@&Ov sN2h^LLd72G1r3QzhacZ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-v11/activity_export.xml b/app/src/main/res/layout-v11/activity_export.xml new file mode 100644 index 0000000..084cba1 --- /dev/null +++ b/app/src/main/res/layout-v11/activity_export.xml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-v11/directory_chooser.xml b/app/src/main/res/layout-v11/directory_chooser.xml new file mode 100644 index 0000000..be82d83 --- /dev/null +++ b/app/src/main/res/layout-v11/directory_chooser.xml @@ -0,0 +1,121 @@ + + + + + + + + + +