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 0000000..0e4f334 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_create.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_create_light.png b/app/src/main/res/drawable-hdpi/ic_action_create_light.png new file mode 100644 index 0000000..81d535d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_create_light.png differ 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 0000000..b6de77e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/navigation_up.png differ 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 0000000..2ef4038 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/navigation_up_light.png differ 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 0000000..2b77cca Binary files /dev/null and b/app/src/main/res/drawable-ldpi/navigation_up.png differ 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 0000000..1d656b9 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/navigation_up_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_create.png b/app/src/main/res/drawable-mdpi/ic_action_create.png new file mode 100644 index 0000000..86097d8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_create.png differ 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 0000000..a4c84f0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_create_light.png differ diff --git a/app/src/main/res/drawable-mdpi/navigation_up.png b/app/src/main/res/drawable-mdpi/navigation_up.png new file mode 100644 index 0000000..b9bbdb2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/navigation_up.png differ 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 0000000..f6c6720 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/navigation_up_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_create.png b/app/src/main/res/drawable-xhdpi/ic_action_create.png new file mode 100644 index 0000000..1ebdb43 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_create.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_create_light.png b/app/src/main/res/drawable-xhdpi/ic_action_create_light.png new file mode 100644 index 0000000..7049567 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_create_light.png differ 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 0000000..f8c3e6f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/navigation_up.png differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_up_light.png b/app/src/main/res/drawable-xhdpi/navigation_up_light.png new file mode 100644 index 0000000..c35765c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/navigation_up_light.png differ 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 0000000..9322b13 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_create.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_create_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_create_light.png new file mode 100644 index 0000000..9322b13 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_create_light.png differ diff --git a/app/src/main/res/drawable/borderless_button.xml b/app/src/main/res/drawable/borderless_button.xml new file mode 100644 index 0000000..9ebbb58 --- /dev/null +++ b/app/src/main/res/drawable/borderless_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + +