/* * Copyright 2014 Yaroslav Mytkalyk * 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.docd.purefm.operations; import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.IBinder; import com.docd.purefm.ActivityMonitor; import com.docd.purefm.R; import com.docd.purefm.file.FileFactory; import com.docd.purefm.file.GenericFile; import com.docd.purefm.services.MultiWorkerService; import com.docd.purefm.settings.Settings; import com.docd.purefm.ui.activities.BrowserPagerActivity; import com.docd.purefm.utils.ArrayUtils; import com.docd.purefm.utils.ClipBoard; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.io.File; /** * IntentService that performs file operations * * @author Doctoror */ public final class OperationsService extends MultiWorkerService implements ActivityMonitor.ActivityMonitorListener { public static final String ACTION_PASTE = "OperationsService.actions.PASTE"; public static final String ACTION_DELETE = "OperationsService.actions.DELETE"; public static final String ACTION_RENAME = "OperationsService.actions.RENAME"; public static final String ACTION_CREATE_FILE = "OperationsService.actions.CREATE_FILE"; public static final String ACTION_CREATE_DIRECTORY = "OperationsService.actions.CREATE_DIRECTORY"; private static final String ACTION_CANCEL_PASTE = "OperationsService.actions.cancel.PASTE"; private static final String ACTION_CANCEL_DELETE = "OperationsService.actions.cancel.DELETE"; private static final String EXTRA_FILE_NAME = "OperationsService.extras.FILE_NAME"; private static final String EXTRA_FILES = "OperationsService.extras.FILES"; private static final String EXTRA_FILE = "OperationsService.extras.FILE"; private static final String EXTRA_IS_MOVE = "OperationsService.extras.IS_MOVE"; private IBinder mLocalBinder; private OperationListener mOperationListener; private PasteOperation mPasteOperation; private DeleteOperation mDeleteOperation; private final Object mOperationListenerLock = new Object(); private Handler mHandler; private OnOperationStartedRunnable mPendingOperationStartedRunnable; private OnOperationEndedRunnable mPendingOperationEndedRunnable; private enum EOperation { PASTE(1), DELETE(2); final int mId; private EOperation(final int id) { mId = id; } } public static void paste(@NonNull final Context context, @NonNull final GenericFile target, @NonNull final GenericFile[] files, final boolean isMove) { final Intent intent = new Intent(context, OperationsService.class); intent.setAction(ACTION_PASTE); intent.putExtra(EXTRA_FILE, target); intent.putExtra(EXTRA_FILES, files); intent.putExtra(EXTRA_IS_MOVE, isMove); context.startService(intent); } public static void cancelPaste(@NonNull final Context context) { context.startService(getCancelPasteIntent(context)); } public static void delete(@NonNull final Context context, @NonNull final GenericFile[] files) { final Intent intent = new Intent(context, OperationsService.class); intent.setAction(ACTION_DELETE); intent.putExtra(EXTRA_FILES, files); context.startService(intent); } public static void cancelDelete(@NonNull final Context context) { context.startService(getCancelDeleteIntent(context)); } public static void rename(@NonNull final Context context, @NonNull final GenericFile source, @NonNull final String targetName) { final Intent intent = new Intent(context, OperationsService.class); intent.setAction(ACTION_RENAME); intent.putExtra(EXTRA_FILE, source); intent.putExtra(EXTRA_FILE_NAME, targetName); context.startService(intent); } public static void createFile(@NonNull final Context context, @NonNull final File parent, @NonNull final String fileName) { final Intent intent = new Intent(context, OperationsService.class); intent.setAction(ACTION_CREATE_FILE); intent.putExtra(EXTRA_FILE, parent); intent.putExtra(EXTRA_FILE_NAME, fileName); context.startService(intent); } public static void createDirectory(@NonNull final Context context, @NonNull final File parent, @NonNull final String dirName) { final Intent intent = new Intent(context, OperationsService.class); intent.setAction(ACTION_CREATE_DIRECTORY); intent.putExtra(EXTRA_FILE, parent); intent.putExtra(EXTRA_FILE_NAME, dirName); context.startService(intent); } @NonNull private static Intent getCancelPasteIntent(@NonNull final Context context) { final Intent intent = new Intent(context, OperationsService.class); intent.setAction(ACTION_CANCEL_PASTE); return intent; } @NonNull private static Intent getCancelDeleteIntent(@NonNull final Context context) { final Intent intent = new Intent(context, OperationsService.class); intent.setAction(ACTION_CANCEL_DELETE); return intent; } @Override public void onCreate() { super.onCreate(); mHandler = new Handler(getMainLooper()); ActivityMonitor.getInstance().registerActivityMonitorListener(this); } @Override public void onDestroy() { super.onDestroy(); ActivityMonitor.getInstance().unregisterActivityMonitorListener(this); } @Override protected void onHandleIntent(@NonNull final Intent intent) { final String action = intent.getAction(); if (action != null) { switch (action) { case ACTION_PASTE: onActionPaste(intent); break; case ACTION_DELETE: onActionDelete(intent); break; case ACTION_RENAME: onActionRename(intent); break; case ACTION_CREATE_FILE: onActionCreateFile(intent); break; case ACTION_CREATE_DIRECTORY: onActionCreateDirectory(intent); break; case ACTION_CANCEL_PASTE: if (mPasteOperation != null) { mPasteOperation.cancel(true); } break; case ACTION_CANCEL_DELETE: if (mDeleteOperation != null) { mDeleteOperation.cancel(true); } break; } } } // only one paste operation simultaneously private synchronized void onActionPaste(@NonNull final Intent pasteIntent) { final GenericFile target = (GenericFile) pasteIntent.getSerializableExtra(EXTRA_FILE); if (target == null) { throw new RuntimeException("ACTION_PASTE intent should contain non-null EXTRA_FILE"); } final Object[] filesObject = (Object[]) pasteIntent.getSerializableExtra(EXTRA_FILES); if (filesObject == null) { throw new RuntimeException("ACTION_PASTE intent should contain non-null EXTRA_FILES"); } final GenericFile[] files = new GenericFile[filesObject.length]; ArrayUtils.copyArrayAndCast(filesObject, files); final boolean isMove = pasteIntent.getBooleanExtra(EXTRA_IS_MOVE, false); mPasteOperation = new PasteOperation(this, target, isMove); synchronized (mOperationListenerLock) { if (mOperationListener != null) { mPendingOperationStartedRunnable = new OnOperationStartedRunnable( ACTION_PASTE, mOperationListener, getOperationMessage(EOperation.PASTE), getCancelPasteIntent(this)); mHandler.removeCallbacks(mPendingOperationEndedRunnable); mHandler.post(mPendingOperationStartedRunnable); } } Object result = null; try { result = mPasteOperation.execute(files); } catch (Throwable e) { e.printStackTrace(); } finally { onOperationCompleted(ACTION_PASTE, result, mPasteOperation.isCanceled()); } } //only one deletion operation can be done simultaneously private synchronized void onActionDelete(@NonNull final Intent deleteIntent) { final Object[] filesObject = (Object[]) deleteIntent.getSerializableExtra(EXTRA_FILES); if (filesObject == null) { throw new RuntimeException("ACTION_DELETE intent should contain non-null EXTRA_FILES"); } final GenericFile[] files = new GenericFile[filesObject.length]; ArrayUtils.copyArrayAndCast(filesObject, files); mDeleteOperation = new DeleteOperation(this); synchronized (mOperationListenerLock) { if (mOperationListener != null) { mPendingOperationStartedRunnable = new OnOperationStartedRunnable( ACTION_DELETE, mOperationListener, getOperationMessage(EOperation.DELETE), getCancelDeleteIntent(this)); mHandler.removeCallbacks(mPendingOperationEndedRunnable); mHandler.post(mPendingOperationStartedRunnable); } } Object result = null; try { result = mDeleteOperation.execute(files); } catch (Throwable e) { e.printStackTrace(); } finally { onOperationCompleted(ACTION_DELETE, result, mDeleteOperation.isCanceled()); } } private void onActionRename(@NonNull final Intent renameIntent) { final GenericFile source = (GenericFile) renameIntent.getSerializableExtra(EXTRA_FILE); final String target = renameIntent.getStringExtra(EXTRA_FILE_NAME); if (source == null || target == null) { throw new RuntimeException( "ACTION_RENAME intent should contain non-null EXTRA_FILE1 and EXTRA_FILE_NAME"); } final RenameOperation renameOperation = new RenameOperation( this, source, target); Object result = null; try { result = renameOperation.execute(); } catch (Throwable e) { e.printStackTrace(); } finally { onOperationCompleted(ACTION_RENAME, result, renameOperation.isCanceled()); } } private void onActionCreateFile(@NonNull final Intent createIntent) { final File parent = (File) createIntent.getSerializableExtra(EXTRA_FILE); if (parent == null) { throw new RuntimeException( "ACTION_CREATE_FILE intent should contain non-null EXTRA_FILE1"); } final String fileName = createIntent.getStringExtra(EXTRA_FILE_NAME); if (fileName == null) { throw new RuntimeException( "ACTION_CREATE_FILE intent should contain non-null EXTRA_FILE_NAME"); } final GenericFile target = FileFactory.newFile(Settings.getInstance(this), parent, fileName); final CreateFileOperation operation = new CreateFileOperation(this); CharSequence result = null; try { result = operation.execute(target); } catch (Throwable e) { e.printStackTrace(); } finally { onOperationCompleted(ACTION_CREATE_FILE, result, false); } } private void onActionCreateDirectory(@NonNull final Intent createIntent) { final File parent = (File) createIntent.getSerializableExtra(EXTRA_FILE); if (parent == null) { throw new RuntimeException( "ACTION_CREATE_DIRECTORY intent should contain non-null EXTRA_FILE1"); } final String fileName = createIntent.getStringExtra(EXTRA_FILE_NAME); if (fileName == null) { throw new RuntimeException( "ACTION_CREATE_DIRECTORY intent should contain non-null EXTRA_FILE_NAME"); } final GenericFile target = FileFactory.newFile( Settings.getInstance(this), parent, fileName); final CreateDirectoryOperation operation = new CreateDirectoryOperation(this); CharSequence result = null; try { result = operation.execute(target); } catch (Throwable e) { e.printStackTrace(); } finally { onOperationCompleted(ACTION_CREATE_DIRECTORY, result, false); } } private void onOperationCompleted(@NonNull final String action, @Nullable final Object result, final boolean wasCanceled) { mHandler.removeCallbacks(mPendingOperationStartedRunnable); synchronized (mOperationListenerLock) { if (mOperationListener != null) { mPendingOperationEndedRunnable = new OnOperationEndedRunnable( mOperationListener, action, result); mHandler.post(mPendingOperationEndedRunnable); } } stopForeground(true); } @Override public void onAtLeastOneActivityStarted() { //stub } @Override public void onAllActivitiesStopped() { if (mPasteOperation != null) { startForeground(EOperation.PASTE); } else if (mDeleteOperation != null) { startForeground(EOperation.DELETE); } } private void startForeground(@NonNull final EOperation operation) { final Context context = getApplicationContext(); if (context == null) { throw new IllegalStateException("getApplicationContext() returned null"); } final Notification.Builder b = new Notification.Builder(context); b.setContentTitle(getText(R.string.app_name)); b.setOngoing(true); b.setProgress(0, 0, true); b.setSmallIcon(R.drawable.ic_stat_notify); b.setContentText(getOperationMessage(operation)); b.setContentIntent(PendingIntent.getActivity(context, 0, new Intent( context, BrowserPagerActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)); startForeground(operation.mId, build(b)); } @SuppressWarnings("deprecation") @SuppressLint("NewApi") @NonNull private static Notification build(@NonNull final Notification.Builder builder) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return builder.build(); } return builder.getNotification(); } private CharSequence getOperationMessage(final EOperation operation) { switch (operation) { case PASTE: final GenericFile[] files = ClipBoard.getClipBoardContents(); if (files != null) { final int textResId = ClipBoard.isMove() ? R.plurals.progress_moving_n_files : R.plurals.progress_copying_n_files; return getResources().getQuantityString( textResId, files.length, files.length); } break; case DELETE: return getText(R.string.progress_deleting_files); default: break; } return null; } @Nullable @Override public IBinder onBind(Intent intent) { if (mLocalBinder == null) { mLocalBinder = new LocalBinder(); } return mLocalBinder; } @Override public boolean onUnbind(Intent intent) { synchronized (mOperationListenerLock) { mOperationListener = null; } return super.onUnbind(intent); } public interface OperationListener { void onOperationStarted(@NonNull String operation, @Nullable CharSequence operationMessage, @NonNull Intent cancelIntent); void onOperationEnded(@Nullable String operation, @Nullable Object result); } private static final class OnOperationStartedRunnable implements Runnable { private final String mOperation; private final CharSequence mOperationMessage; private final Intent mCancelIntent; private final OperationListener mOperationListener; OnOperationStartedRunnable(@NonNull final String operation, @NonNull OperationListener operationListener, @Nullable final CharSequence operationMessage, @NonNull final Intent cancelIntent) { mOperation = operation; mOperationListener = operationListener; mOperationMessage = operationMessage; mCancelIntent = cancelIntent; } @Override public void run() { mOperationListener.onOperationStarted(mOperation, mOperationMessage, mCancelIntent); } } private static final class OnOperationEndedRunnable implements Runnable { @NonNull private final String mOperation; @Nullable private final Object mResult; @NonNull private final OperationListener mOperationListener; OnOperationEndedRunnable(@NonNull OperationListener operationListener, @NonNull final String operation, @Nullable final Object result) { this.mOperationListener = operationListener; this.mOperation = operation; this.mResult = result; } @Override public void run() { mOperationListener.onOperationEnded(mOperation, mResult); } } public final class LocalBinder extends Binder { public void setOperationListener(@Nullable final OperationListener l) { if (l != null) { if (mDeleteOperation != null) { l.onOperationStarted(ACTION_DELETE, getOperationMessage(EOperation.DELETE), getCancelDeleteIntent(getApplicationContext())); } else if (mPasteOperation != null) { l.onOperationStarted(ACTION_PASTE, getOperationMessage(EOperation.PASTE), getCancelPasteIntent(getApplicationContext())); } else { l.onOperationEnded(null, null); } } synchronized (mOperationListenerLock) { mOperationListener = l; } } } }