/* * Copyright (C) 2012 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.cyanogenmod.filemanager.ui.policy; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.text.Html; import android.text.Spanned; import android.widget.Toast; import com.cyanogenmod.filemanager.FileManagerApplication; import com.cyanogenmod.filemanager.R; import com.cyanogenmod.filemanager.commands.AsyncResultListener; import com.cyanogenmod.filemanager.commands.CompressExecutable; import com.cyanogenmod.filemanager.commands.UncompressExecutable; import com.cyanogenmod.filemanager.console.ConsoleBuilder; import com.cyanogenmod.filemanager.console.ExecutionException; import com.cyanogenmod.filemanager.console.RelaunchableException; import com.cyanogenmod.filemanager.listeners.OnRequestRefreshListener; import com.cyanogenmod.filemanager.listeners.OnSelectionListener; import com.cyanogenmod.filemanager.model.FileSystemObject; import com.cyanogenmod.filemanager.preferences.CompressionMode; import com.cyanogenmod.filemanager.util.CommandHelper; import com.cyanogenmod.filemanager.util.DialogHelper; import com.cyanogenmod.filemanager.util.ExceptionUtil; import com.cyanogenmod.filemanager.util.ExceptionUtil.OnRelaunchCommandResult; import com.cyanogenmod.filemanager.util.FileHelper; import com.cyanogenmod.filemanager.util.FixedQueue; import java.io.File; import java.util.ArrayList; import java.util.List; /** * A class with the convenience methods for resolve compress/uncompress related actions */ public final class CompressActionPolicy extends ActionsPolicy { /** * A class that holds a listener for compression/uncompression operations */ private static class CompressListener implements AsyncResultListener { final FixedQueue<String> mQueue; boolean mEnd; Throwable mCause; /** * Constructor of <code>CompressListener</code> */ public CompressListener() { super(); this.mEnd = false; this.mQueue = new FixedQueue<String>(2); //Holds only one item this.mCause = null; } @Override public void onPartialResult(Object result) { this.mQueue.insert((String)result); } @Override public void onException(Exception cause) { this.mCause = cause; } @Override public void onAsyncStart() {/**NON BLOCK**/} @Override public void onAsyncEnd(boolean cancelled) {/**NON BLOCK**/} @Override public void onAsyncExitCode(int exitCode) { this.mEnd = true; } } /** * Method that compresses the list of files of the selection. * * @param ctx The current context * @param onSelectionListener The listener for obtain selection information (required) * @param onRequestRefreshListener The listener for request a refresh (optional) * @hide */ public static void compress( final Context ctx, final OnSelectionListener onSelectionListener, final OnRequestRefreshListener onRequestRefreshListener) { // Retrieve the current selection final List<FileSystemObject> selection = onSelectionListener.onRequestSelectedFiles(); if (selection != null && selection.size() > 0) { // Show a dialog to allow the user make the compression mode choice AlertDialog dialog = DialogHelper.createSingleChoiceDialog( ctx, R.string.compression_mode_title, getSupportedCompressionModesLabels(ctx, selection), CompressionMode.AC_GZIP.ordinal(), new DialogHelper.OnSelectChoiceListener() { @Override public void onSelectChoice(int choice) { // Do the compression compress( ctx, getCompressionModeFromUserChoice(choice), selection, onSelectionListener, onRequestRefreshListener); } @Override public void onNoSelectChoice() {/**NON BLOCK**/} }); DialogHelper.delegateDialogShow(ctx, dialog); } } /** * Method that compresses an uncompressed file. * * @param ctx The current context * @param fso The compressed file * @param onSelectionListener The listener for obtain selection information (required) * @param onRequestRefreshListener The listener for request a refresh (optional) * @hide */ public static void compress( final Context ctx, final FileSystemObject fso, final OnSelectionListener onSelectionListener, final OnRequestRefreshListener onRequestRefreshListener) { // Create a list with the item final List<FileSystemObject> items = new ArrayList<FileSystemObject>(); items.add(fso); // Show a dialog to allow the user make the compression mode choice AlertDialog dialog = DialogHelper.createSingleChoiceDialog( ctx, R.string.compression_mode_title, getSupportedCompressionModesLabels(ctx, items), CompressionMode.AC_GZIP.ordinal(), new DialogHelper.OnSelectChoiceListener() { @Override public void onSelectChoice(int choice) { // Do the compression compress( ctx, getCompressionModeFromUserChoice(choice), items, onSelectionListener, onRequestRefreshListener); } @Override public void onNoSelectChoice() {/**NON BLOCK**/} }); DialogHelper.delegateDialogShow(ctx, dialog); } /** * Method that compresses some uncompressed files or folders * * @param ctx The current context * @param mode The compression mode * @param fsos The list of files to compress * @param onSelectionListener The listener for obtain selection information (required) * @param onRequestRefreshListener The listener for request a refresh (optional) * @hide */ static void compress( final Context ctx, final CompressionMode mode, final List<FileSystemObject> fsos, final OnSelectionListener onSelectionListener, final OnRequestRefreshListener onRequestRefreshListener) { // The callable interface final BackgroundCallable callable = new BackgroundCallable() { // The current items final Context mCtx = ctx; final CompressionMode mMode = mode; final List<FileSystemObject> mFsos = fsos; final OnRequestRefreshListener mOnRequestRefreshListener = onRequestRefreshListener; final Object mSync = new Object(); Throwable mCause; CompressExecutable cmd = null; final CompressListener mListener = new CompressListener(); private String mMsg; private boolean mStarted = false; @Override public int getDialogTitle() { return R.string.waiting_dialog_compressing_title; } @Override public int getDialogIcon() { return 0; } @Override public boolean isDialogCancellable() { return true; } @Override public Spanned requestProgress() { // Initializing the dialog if (!this.mStarted) { String progress = this.mCtx.getResources(). getString( R.string.waiting_dialog_analizing_msg); return Html.fromHtml(progress); } // Return the current operation String msg = (this.mMsg == null) ? "" : this.mMsg; //$NON-NLS-1$ String progress = this.mCtx.getResources(). getString( R.string.waiting_dialog_compressing_msg, msg); return Html.fromHtml(progress); } @Override public void onSuccess() { try { if (this.cmd != null && this.cmd.isCancellable() && !this.cmd.isCancelled()) { this.cmd.cancel(); } } catch (Exception e) {/**NON BLOCK**/} //Operation complete. Refresh if (this.mOnRequestRefreshListener != null) { // The reference is not the same, so refresh the complete navigation view this.mOnRequestRefreshListener.onRequestRefresh(null, true); } if (this.cmd != null) { showOperationSuccessMsg( ctx, R.string.msgs_compressing_success, this.cmd.getOutCompressedFile()); } else { ActionsPolicy.showOperationSuccessMsg(ctx); } } @Override public void doInBackground(Object... params) throws Throwable { this.mCause = null; this.mStarted = true; // This method expect to receive // 1.- BackgroundAsyncTask BackgroundAsyncTask task = (BackgroundAsyncTask)params[0]; String out = null; try { // Archive or Archive-Compression if (this.mMode.mArchive) { // Convert the list to an array of full paths String[] src = new String[this.mFsos.size()]; int cc = this.mFsos.size(); for (int i = 0; i < cc; i++) { src[i] = this.mFsos.get(i).getFullPath(); } // Use the current directory name for create the compressed file String curDirName = new File(onSelectionListener.onRequestCurrentDir()).getName(); if (src.length == 1) { // But only one file is passed, then used the name of unique file curDirName = FileHelper.getName(this.mFsos.get(0).getName()); } String name = String.format( "%s.%s", curDirName, this.mMode.mExtension); //$NON-NLS-1$ String newName = FileHelper.createNonExistingName( ctx, onSelectionListener.onRequestCurrentItems(), name, R.string.create_new_compress_file_regexp); String newNameAbs = new File( onSelectionListener.onRequestCurrentDir(), newName).getAbsolutePath(); // Do the compression this.cmd = CommandHelper.compress( ctx, this.mMode, newNameAbs, src, this.mListener, null); // Compression } else { // Only the first item from the list is valid. If there are more in the // list, then discard them String src = this.mFsos.get(0).getFullPath(); // Do the compression this.cmd = CommandHelper.compress( ctx, this.mMode, src, this.mListener, null); } out = this.cmd.getOutCompressedFile(); // Request paint the this.mListener.mQueue.insert(out); task.onRequestProgress(); // Don't use an active blocking because this suppose that all message // will be processed by the UI. Instead, refresh with a delay and // display the active file while (!this.mListener.mEnd) { // Sleep to don't saturate the UI thread Thread.sleep(50L); List<String> msgs = this.mListener.mQueue.peekAll(); if (msgs.size() > 0) { this.mMsg = msgs.get(msgs.size()-1); task.onRequestProgress(); } } // Dialog is ended. Force the last redraw List<String> msgs = this.mListener.mQueue.peekAll(); if (msgs.size() > 0) { this.mMsg = msgs.get(msgs.size()-1); task.onRequestProgress(); } } catch (Exception e) { // Need to be relaunched? if (e instanceof RelaunchableException) { OnRelaunchCommandResult rl = new OnRelaunchCommandResult() { @Override @SuppressWarnings("unqualified-field-access") public void onSuccess() { synchronized (mSync) { mSync.notify(); } } @Override @SuppressWarnings("unqualified-field-access") public void onFailed(Throwable cause) { mCause = cause; synchronized (mSync) { mSync.notify(); } } @Override @SuppressWarnings("unqualified-field-access") public void onCancelled() { synchronized (mSync) { mSync.notify(); } } }; // Translate the exception (and wait for the result) ExceptionUtil.translateException(ctx, e, false, true, rl); synchronized (this.mSync) { this.mSync.wait(); } // Persist the exception? if (this.mCause != null) { // The exception must be elevated throw this.mCause; } } else { // The exception must be elevated throw e; } } // Any exception? if (this.mListener.mCause != null) { throw this.mListener.mCause; } // Check that the operation was completed retrieving the compressed file or folder boolean failed = false; try { FileSystemObject fso = CommandHelper.getFileInfo(ctx, out, false, null); if (fso == null) { // Failed. The file or folder not exists failed = true; } // Operation complete successfully } catch (Throwable e) { // Failed. The file or folder not exists failed = true; } if (failed) { throw new ExecutionException( String.format( "Failed to compress file(s) to: %s", out)); //$NON-NLS-1$ } } }; final BackgroundAsyncTask task = new BackgroundAsyncTask(ctx, callable); // Check if the output exists. When the mode is archive, this method generate a new // name based in the current directory. When the mode is compressed then the name // is the name of the file to compress without extension. In this case the name should // be validate prior to compress boolean askUser = false; try { if (!mode.mArchive) { // Only the first item from the list is valid. If there are more in the // list, then discard them String src = fsos.get(0).getFullPath(); CompressExecutable ucmd = FileManagerApplication.getBackgroundConsole(). getExecutableFactory().newCreator(). createCompressExecutable(mode, src, null); String dst = ucmd.getOutCompressedFile(); FileSystemObject info = CommandHelper.getFileInfo(ctx, dst, null); if (info != null) { askUser = true; } } } catch (Exception e) {/**NON BLOCK**/} // Ask the user because the destination file or folder exists if (askUser) { //Show a dialog asking the user for overwrite the files AlertDialog dialog = DialogHelper.createTwoButtonsQuestionDialog( ctx, android.R.string.cancel, R.string.overwrite, R.string.confirm_overwrite, ctx.getString(R.string.msgs_overwrite_files), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface alertDialog, int which) { // NEGATIVE (overwrite) POSITIVE (cancel) if (which == DialogInterface.BUTTON_NEGATIVE) { // Execute background task task.execute(task); } } }); DialogHelper.delegateDialogShow(ctx, dialog); } else { // Execute background task task.execute(task); } } /** * Method that uncompress a compressed file. * * @param ctx The current context * @param fso The compressed file * @param onRequestRefreshListener The listener for request a refresh (optional) * @hide */ public static void uncompress( final Context ctx, final FileSystemObject fso, final OnRequestRefreshListener onRequestRefreshListener) { // The callable interface final BackgroundCallable callable = new BackgroundCallable() { // The current items final Context mCtx = ctx; final FileSystemObject mFso = fso; final OnRequestRefreshListener mOnRequestRefreshListener = onRequestRefreshListener; final Object mSync = new Object(); Throwable mCause; UncompressExecutable cmd; final CompressListener mListener = new CompressListener(); private String mMsg; private boolean mStarted = false; @Override public int getDialogTitle() { return R.string.waiting_dialog_extracting_title; } @Override public int getDialogIcon() { return 0; } @Override public boolean isDialogCancellable() { return true; } @Override public Spanned requestProgress() { // Initializing the dialog if (!this.mStarted) { String progress = this.mCtx.getResources(). getString( R.string.waiting_dialog_analizing_msg); return Html.fromHtml(progress); } // Return the current operation String msg = (this.mMsg == null) ? "" : this.mMsg; //$NON-NLS-1$ String progress = this.mCtx.getResources(). getString( R.string.waiting_dialog_extracting_msg, msg); return Html.fromHtml(progress); } @Override public void onSuccess() { try { if (this.cmd != null && this.cmd.isCancellable() && !this.cmd.isCancelled()) { this.cmd.cancel(); } } catch (Exception e) {/**NON BLOCK**/} //Operation complete. Refresh if (this.mOnRequestRefreshListener != null) { // The reference is not the same, so refresh the complete navigation view this.mOnRequestRefreshListener.onRequestRefresh(null, true); } if (this.cmd != null) { showOperationSuccessMsg( ctx, R.string.msgs_extracting_success, this.cmd.getOutUncompressedFile()); } else { ActionsPolicy.showOperationSuccessMsg(ctx); } } @Override public void doInBackground(Object... params) throws Throwable { this.mCause = null; this.mStarted = true; // This method expect to receive // 1.- BackgroundAsyncTask BackgroundAsyncTask task = (BackgroundAsyncTask)params[0]; String out = null; try { this.cmd = CommandHelper.uncompress( ctx, this.mFso.getFullPath(), null, this.mListener, null); out = this.cmd.getOutUncompressedFile(); // Request paint the this.mListener.mQueue.insert(out); task.onRequestProgress(); // Don't use an active blocking because this suppose that all message // will be processed by the UI. Instead, refresh with a delay and // display the active file while (!this.mListener.mEnd) { // Sleep to don't saturate the UI thread Thread.sleep(50L); List<String> msgs = this.mListener.mQueue.peekAll(); if (msgs.size() > 0) { this.mMsg = msgs.get(msgs.size()-1); task.onRequestProgress(); } } // Dialog is ended. Force the last redraw List<String> msgs = this.mListener.mQueue.peekAll(); if (msgs.size() > 0) { this.mMsg = msgs.get(msgs.size()-1); task.onRequestProgress(); } } catch (Exception e) { // Need to be relaunched? if (e instanceof RelaunchableException) { OnRelaunchCommandResult rl = new OnRelaunchCommandResult() { @Override @SuppressWarnings("unqualified-field-access") public void onSuccess() { synchronized (mSync) { mSync.notify(); } } @Override @SuppressWarnings("unqualified-field-access") public void onFailed(Throwable cause) { mCause = cause; synchronized (mSync) { mSync.notify(); } } @Override @SuppressWarnings("unqualified-field-access") public void onCancelled() { synchronized (mSync) { mSync.notify(); } } }; // Translate the exception (and wait for the result) ExceptionUtil.translateException(ctx, e, false, true, rl); synchronized (this.mSync) { this.mSync.wait(); } // Persist the exception? if (this.mCause != null) { // The exception must be elevated throw this.mCause; } } else { // The exception must be elevated throw e; } } // Any exception? if (this.mListener.mCause != null) { throw this.mListener.mCause; } // Check that the operation was completed retrieving the uncompressed // file or folder boolean failed = false; try { FileSystemObject fso2 = CommandHelper.getFileInfo(ctx, out, false, null); if (fso2 == null) { // Failed. The file or folder not exists failed = true; } // Operation complete successfully } catch (Throwable e) { // Failed. The file or folder not exists failed = true; } if (failed) { throw new ExecutionException( String.format( "Failed to extract file: %s", //$NON-NLS-1$ this.mFso.getFullPath())); } } }; final BackgroundAsyncTask task = new BackgroundAsyncTask(ctx, callable); // Check if the output exists boolean askUser = false; try { UncompressExecutable ucmd = FileManagerApplication.getBackgroundConsole(). getExecutableFactory().newCreator(). createUncompressExecutable(fso.getFullPath(), null, null); String dst = ucmd.getOutUncompressedFile(); FileSystemObject info = CommandHelper.getFileInfo(ctx, dst, null); if (info != null) { askUser = true; } } catch (Exception e) {/**NON BLOCK**/} // Ask the user because the destination file or folder exists if (askUser) { //Show a dialog asking the user for overwrite the files AlertDialog dialog = DialogHelper.createTwoButtonsQuestionDialog( ctx, android.R.string.cancel, R.string.overwrite, R.string.confirm_overwrite, ctx.getString(R.string.msgs_overwrite_files), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface alertDialog, int which) { // NEGATIVE (overwrite) POSITIVE (cancel) if (which == DialogInterface.BUTTON_NEGATIVE) { // Check if the necessary to display a warning because // security issues checkZipSecurityWarning(ctx, task, fso); } } }); DialogHelper.delegateDialogShow(ctx, dialog); } else { // Execute background task task.execute(task); } } /** * Method that checks if it is necessary to display a warning dialog because * the privileged extraction of a zip file. * * @param ctx The current context * @param task The task * @param fso The zip file * @hide */ static void checkZipSecurityWarning( final Context ctx, final BackgroundAsyncTask task, FileSystemObject fso) { // WARNING! Extracting a ZIP file with relatives or absolutes path could break // the system and is need a security alert that the user can confirm prior to // make the extraction String ext = FileHelper.getExtension(fso); if (ConsoleBuilder.isPrivileged() && ext.compareToIgnoreCase("zip") == 0) { //$NON-NLS-1$ AlertDialog dialog = DialogHelper.createYesNoDialog( ctx, R.string.confirm_overwrite, R.string.security_warning_extract, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface alertDialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { // Execute background task task.execute(task); } } }); dialog.show(); } else { // Execute background task task.execute(task); } } /** * Method that returns the supported compression modes * * @param ctx The current context * @param fsos The list of file system objects to compress * @return String[] An array with the compression mode labels */ private static String[] getSupportedCompressionModesLabels( Context ctx, List<FileSystemObject> fsos) { String[] labels = ctx.getResources().getStringArray(R.array.compression_modes_labels); if (fsos.size() > 1 || (fsos.size() == 1 && FileHelper.isDirectory(fsos.get(0)))) { // If more that a file is requested, compression is not available // The same applies if the unique item is a folder ArrayList<String> validLabels = new ArrayList<String>(); CompressionMode[] values = CompressionMode.values(); int cc = values.length; for (int i = 0; i < cc; i++) { if (values[i].mArchive) { validLabels.add(labels[i]); } } labels = validLabels.toArray(new String[]{}); } return labels; } /** * Method that returns the compression mode from the user choice * * @param choice The choice of the user * @return CompressionMode The compression mode */ static CompressionMode getCompressionModeFromUserChoice(int choice) { CompressionMode[] values = CompressionMode.values(); int cc = values.length; for (int i = 0; i < cc; i++) { if (values[i].ordinal() == choice) { return values[i]; } } return null; } /** * Method that shows a message when the operation is complete successfully * * @param ctx The current context * @param res The resource identifier * @param dst The destination output * @hide */ protected static void showOperationSuccessMsg(Context ctx, int res, String dst) { DialogHelper.showToast(ctx, ctx.getString(res, dst), Toast.LENGTH_SHORT); } }