/* * Copyright (C) 2014 Arpit Khurana <arpitkh96@gmail.com>, Vishal Nehra <vishalmeham2@gmail.com>, * Emmanuel Messulam<emmanuelbendavid@gmail.com> * * This file is part of Amaze File Manager. * * Amaze File Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.amaze.filemanager.services; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.AsyncTask; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.text.format.Formatter; import android.util.Log; import com.amaze.filemanager.R; import com.amaze.filemanager.activities.BaseActivity; import com.amaze.filemanager.activities.MainActivity; import com.amaze.filemanager.database.CryptHandler; import com.amaze.filemanager.database.EncryptedEntry; import com.amaze.filemanager.exceptions.RootNotPermittedException; import com.amaze.filemanager.filesystem.BaseFile; import com.amaze.filemanager.filesystem.FileUtil; import com.amaze.filemanager.filesystem.HFile; import com.amaze.filemanager.filesystem.Operations; import com.amaze.filemanager.filesystem.RootHelper; import com.amaze.filemanager.utils.AppConfig; import com.amaze.filemanager.utils.CloudUtil; import com.amaze.filemanager.utils.CryptUtil; import com.amaze.filemanager.utils.DataPackage; import com.amaze.filemanager.utils.Futils; import com.amaze.filemanager.utils.GenericCopyUtil; import com.amaze.filemanager.utils.OpenMode; import com.amaze.filemanager.utils.ProgressHandler; import com.amaze.filemanager.utils.RootUtils; import com.amaze.filemanager.utils.ServiceWatcherUtil; import java.io.File; import java.io.IOException; import java.util.ArrayList; public class CopyService extends Service { public static final String TAG_COPY_TARGET = "COPY_DIRECTORY"; public static final String TAG_COPY_SOURCES = "FILE_PATHS"; public static final String TAG_COPY_OPEN_MODE = "MODE"; // target open mode public static final String TAG_COPY_MOVE = "move"; private static final String TAG_COPY_START_ID = "id"; public static final String TAG_BROADCAST_COPY_CANCEL = "copycancel"; // list of data packages, to initiate chart in process viewer fragment private ArrayList<DataPackage> dataPackages = new ArrayList<>(); private NotificationManager mNotifyManager; private NotificationCompat.Builder mBuilder; private Context c; private ProgressListener progressListener; private final IBinder mBinder = new LocalBinder(); private ProgressHandler progressHandler; private ServiceWatcherUtil watcherUtil; private long totalSize = 0L; private int totalSourceFiles = 0; private int sourceProgress = 0; @Override public void onCreate() { super.onCreate(); c = getApplicationContext(); registerReceiver(receiver3, new IntentFilter(TAG_BROADCAST_COPY_CANCEL)); } @Override public int onStartCommand(Intent intent, int flags, final int startId) { Bundle b = new Bundle(); ArrayList<BaseFile> files = intent.getParcelableArrayListExtra(TAG_COPY_SOURCES); String targetPath = intent.getStringExtra(TAG_COPY_TARGET); int mode = intent.getIntExtra(TAG_COPY_OPEN_MODE, OpenMode.UNKNOWN.ordinal()); final boolean move = intent.getBooleanExtra(TAG_COPY_MOVE, false); mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); b.putInt(TAG_COPY_START_ID, startId); Intent notificationIntent = new Intent(this, MainActivity.class); notificationIntent.setAction(Intent.ACTION_MAIN); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notificationIntent.putExtra(MainActivity.KEY_INTENT_PROCESS_VIEWER, true); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); mBuilder = new NotificationCompat.Builder(c); mBuilder.setContentIntent(pendingIntent); mBuilder.setContentTitle(getResources().getString(R.string.copying)) .setSmallIcon(R.drawable.ic_content_copy_white_36dp); startForeground(Integer.parseInt("456" + startId), mBuilder.build()); b.putBoolean(TAG_COPY_MOVE, move); b.putString(TAG_COPY_TARGET, targetPath); b.putInt(TAG_COPY_OPEN_MODE, mode); b.putParcelableArrayList(TAG_COPY_SOURCES, files); //going async new DoInBackground().execute(b); // If we get killed, after returning from here, restart return START_STICKY; } /** * Helper method to calculate source files size * @param files * @param context * @return */ long getTotalBytes(ArrayList<BaseFile> files, Context context) { long totalBytes = 0; for (BaseFile file : files) { if (file.isDirectory()) totalBytes += file.folderSize(context); else totalBytes += file.length(context); } return totalBytes; } public void onDestroy() { this.unregisterReceiver(receiver3); } private class DoInBackground extends AsyncTask<Bundle, Void, Integer> { ArrayList<BaseFile> sourceFiles; boolean move; Copy copy; private String targetPath; private OpenMode openMode; protected Integer doInBackground(Bundle... p1) { sourceFiles = p1[0].getParcelableArrayList(TAG_COPY_SOURCES); final int id = p1[0].getInt(TAG_COPY_START_ID); // setting up service watchers and initial data packages // finding total size on background thread (this is necessary condition for SMB!) totalSize = getTotalBytes(sourceFiles, c); totalSourceFiles = sourceFiles.size(); progressHandler = new ProgressHandler(totalSourceFiles, totalSize); progressHandler.setProgressListener(new ProgressHandler.ProgressListener() { @Override public void onProgressed(String fileName, int sourceFiles, int sourceProgress, long totalSize, long writtenSize, int speed) { publishResults(id, fileName, sourceFiles, sourceProgress, totalSize, writtenSize, speed, false, move); } }); watcherUtil = new ServiceWatcherUtil(progressHandler, totalSize); DataPackage intent1 = new DataPackage(); intent1.setName(sourceFiles.get(0).getName()); intent1.setSourceFiles(sourceFiles.size()); intent1.setSourceProgress(0); intent1.setTotal(totalSize); intent1.setByteProgress(0); intent1.setSpeedRaw(0); intent1.setMove(move); intent1.setCompleted(false); putDataPackage(intent1); targetPath = p1[0].getString(TAG_COPY_TARGET); move = p1[0].getBoolean(TAG_COPY_MOVE); openMode = OpenMode.getOpenMode(p1[0].getInt(TAG_COPY_OPEN_MODE)); copy = new Copy(); copy.execute(sourceFiles, targetPath, move, openMode); if (copy.failedFOps.size() == 0) { // adding/updating new encrypted db entry if any encrypted file was copied/moved for (BaseFile sourceFile : sourceFiles) { findAndReplaceEncryptedEntry(sourceFile); } } return id; } @Override public void onPostExecute(Integer b) { super.onPostExecute(b); // publishResults(b, "", totalSourceFiles, totalSourceFiles, totalSize, totalSize, 0, true, move); // stopping watcher if not yet finished watcherUtil.stopWatch(); generateNotification(copy.failedFOps, move); Intent intent = new Intent("loadlist"); sendBroadcast(intent); stopSelf(); } /** * Iterates through every file to find an encrypted file and update/add a new entry about it's * metadata in the database * @param sourceFile the file which is to be iterated */ private void findAndReplaceEncryptedEntry(BaseFile sourceFile) { // even directories can end with CRYPT_EXTENSION if (sourceFile.isDirectory() && !sourceFile.getName().endsWith(CryptUtil.CRYPT_EXTENSION)) { for (BaseFile file : sourceFile.listFiles(getApplicationContext(), BaseActivity.rootMode)) { // iterating each file inside source files which were copied to find instance of // any copied / moved encrypted file findAndReplaceEncryptedEntry(file); } } else { if (sourceFile.getName().endsWith(CryptUtil.CRYPT_EXTENSION)) { try { CryptHandler cryptHandler = new CryptHandler(getApplicationContext()); EncryptedEntry oldEntry = cryptHandler.findEntry(sourceFile.getPath()); EncryptedEntry newEntry = new EncryptedEntry(); newEntry.setPassword(oldEntry.getPassword()); newEntry.setPath(targetPath + "/" + sourceFile.getName()); if (move) { // file was been moved, update the existing entry newEntry.setId(oldEntry.getId()); cryptHandler.updateEntry(oldEntry, newEntry); } else { // file was copied, create a new entry with same data cryptHandler.addEntry(newEntry); } } catch (Exception e) { e.printStackTrace(); // couldn't change the entry, leave it alone } } } } class Copy { ArrayList<HFile> failedFOps; ArrayList<BaseFile> toDelete; Copy() { failedFOps = new ArrayList<>(); toDelete = new ArrayList<>(); } /** * Method iterate through files to be copied * * @param sourceFiles * @param targetPath * @param move * @param mode target file open mode (current path's open mode) */ public void execute(final ArrayList<BaseFile> sourceFiles, final String targetPath, final boolean move, OpenMode mode) { // initial start of copy, initiate the watcher watcherUtil.watch(); if (FileUtil.checkFolder((targetPath), c) == 1) { for (int i = 0; i < sourceFiles.size(); i++) { sourceProgress = i; BaseFile f1 = (sourceFiles.get(i)); Log.e("Copy", "basefile\t" + f1.getPath()); try { HFile hFile; if (targetPath.contains(getExternalCacheDir().getPath())) { // the target open mode is not the one we're currently in! // we're processing the file for cache hFile = new HFile(OpenMode.FILE, targetPath, sourceFiles.get(i).getName(), f1.isDirectory()); } else { // the target open mode is where we're currently at hFile = new HFile(mode, targetPath, sourceFiles.get(i).getName(), f1.isDirectory()); } if (!progressHandler.getCancelled()) { if (!f1.isSmb() && (f1.getMode() == OpenMode.ROOT || mode == OpenMode.ROOT) && BaseActivity.rootMode) { // either source or target are in root progressHandler.setSourceFilesProcessed(++sourceProgress); copyRoot(f1, hFile, move); continue; } progressHandler.setSourceFilesProcessed(++sourceProgress); copyFiles((f1), hFile, progressHandler); } else { break; } } catch (Exception e) { e.printStackTrace(); Log.e("Copy", "Got exception checkout"); failedFOps.add(sourceFiles.get(i)); for (int j = i + 1; j < sourceFiles.size(); j++) failedFOps.add(sourceFiles.get(j)); break; } } } else if (BaseActivity.rootMode) { for (int i = 0; i < sourceFiles.size(); i++) { if (!progressHandler.getCancelled()) { HFile hFile = new HFile(mode, targetPath, sourceFiles.get(i).getName(), sourceFiles.get(i).isDirectory()); progressHandler.setSourceFilesProcessed(++sourceProgress); progressHandler.setFileName(sourceFiles.get(i).getName()); copyRoot(sourceFiles.get(i), hFile, move); /*if(checkFiles(new HFile(sourceFiles.get(i).getMode(),path), new HFile(OpenMode.ROOT,targetPath+"/"+name))){ failedFOps.add(sourceFiles.get(i)); }*/ } } } else { for (BaseFile f : sourceFiles) failedFOps.add(f); return; } // making sure to delete files after copy operation is done // and not if the copy was cancelled if (move && !progressHandler.getCancelled()) { ArrayList<BaseFile> toDelete = new ArrayList<>(); for (BaseFile a : sourceFiles) { if (!failedFOps.contains(a)) toDelete.add(a); } new DeleteTask(getContentResolver(), c).execute((toDelete)); } } void copyRoot(BaseFile sourceFile, HFile targetFile, boolean move) { try { if (!move) RootUtils.copy(sourceFile.getPath(), targetFile.getPath()); else if (move) RootUtils.move(sourceFile.getPath(), targetFile.getPath()); ServiceWatcherUtil.POSITION += sourceFile.getSize(); } catch (RootNotPermittedException e) { failedFOps.add(sourceFile); e.printStackTrace(); } Futils.scanFile(targetFile.getPath(), c); } private void copyFiles(final BaseFile sourceFile, final HFile targetFile, ProgressHandler progressHandler) throws IOException { if (sourceFile.isDirectory()) { if (progressHandler.getCancelled()) return; if (!targetFile.exists()) targetFile.mkdir(c); // various checks // 1. source file and target file doesn't end up in loop // 2. source file has a valid name or not if (!Operations.isFileNameValid(sourceFile.getName()) || Operations.isCopyLoopPossible(sourceFile, targetFile)) { failedFOps.add(sourceFile); return; } targetFile.setLastModified(sourceFile.lastModified()); if(progressHandler.getCancelled()) return; ArrayList<BaseFile> filePaths = sourceFile.listFiles(c, false); for (BaseFile file : filePaths) { HFile destFile = new HFile(targetFile.getMode(), targetFile.getPath(), file.getName(), file.isDirectory()); copyFiles(file, destFile, progressHandler); } } else { if (progressHandler.getCancelled()) return; if (!Operations.isFileNameValid(sourceFile.getName())) { failedFOps.add(sourceFile); return; } GenericCopyUtil copyUtil = new GenericCopyUtil(c); progressHandler.setFileName(sourceFile.getName()); copyUtil.copy(sourceFile, targetFile); } } } } /** * Displays a notification, sends intent and cancels progress if there were some failures * in copy progress * * @param failedOps * @param move */ void generateNotification(ArrayList<HFile> failedOps, boolean move) { mNotifyManager.cancelAll(); if(failedOps.size()==0) return; NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(c); mBuilder.setContentTitle(c.getString(R.string.operationunsuccesful)); mBuilder.setContentText(c.getString(R.string.copy_error).replace("%s", move ? c.getString(R.string.moved) : c.getString(R.string.copied))); mBuilder.setAutoCancel(true); progressHandler.setCancelled(true); Intent intent= new Intent(this, MainActivity.class); intent.putExtra(MainActivity.TAG_INTENT_FILTER_FAILED_OPS, failedOps); intent.putExtra("move", move); PendingIntent pIntent = PendingIntent.getActivity(this, 101, intent, PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent); mBuilder.setSmallIcon(R.drawable.ic_content_copy_white_36dp); mNotifyManager.notify(741, mBuilder.build()); intent=new Intent(MainActivity.TAG_INTENT_FILTER_GENERAL); intent.putExtra(MainActivity.TAG_INTENT_FILTER_FAILED_OPS, failedOps); intent.putExtra(TAG_COPY_MOVE, move); sendBroadcast(intent); } /** * Publish the results of the progress to notification and {@link DataPackage} * and eventually to {@link com.amaze.filemanager.fragments.ProcessViewer} * * @param id id of current service * @param fileName file name of current file being copied * @param sourceFiles total number of files selected by user for copy * @param sourceProgress files been copied out of them * @param totalSize total size of selected items to copy * @param writtenSize bytes successfully copied * @param speed number of bytes being copied per sec * @param isComplete whether operation completed or ongoing (not supported at the moment) * @param move if the files are to be moved */ private void publishResults(int id, String fileName, int sourceFiles, int sourceProgress, long totalSize, long writtenSize, int speed, boolean isComplete, boolean move) { if (!progressHandler.getCancelled()) { //notification float progressPercent = ((float) writtenSize / totalSize) * 100; mBuilder.setProgress(100, Math.round(progressPercent), false); mBuilder.setOngoing(true); int title = R.string.copying; if (move) title = R.string.moving; mBuilder.setContentTitle(c.getResources().getString(title)); mBuilder.setContentText(fileName + " " + Formatter.formatFileSize(c, writtenSize) + "/" + Formatter.formatFileSize(c, totalSize)); int id1 = Integer.parseInt("456" + id); mNotifyManager.notify(id1, mBuilder.build()); if (writtenSize == totalSize || totalSize == 0) { if (move) { //mBuilder.setContentTitle(getString(R.string.move_complete)); // set progress to indeterminate as deletion might still be going on from source mBuilder.setProgress(0, 0, true); } else { mBuilder.setContentTitle(getString(R.string.copy_complete)); mBuilder.setProgress(0, 0, false); } mBuilder.setContentText(""); mBuilder.setOngoing(false); mBuilder.setAutoCancel(true); mNotifyManager.notify(id1, mBuilder.build()); publishCompletedResult(id1); } //for processviewer DataPackage intent = new DataPackage(); intent.setName(fileName); intent.setSourceFiles(sourceFiles); intent.setSourceProgress(sourceProgress); intent.setTotal(totalSize); intent.setByteProgress(writtenSize); intent.setSpeedRaw(speed); intent.setMove(move); intent.setCompleted(isComplete); putDataPackage(intent); if (progressListener != null) { progressListener.onUpdate(intent); if (isComplete) progressListener.refresh(); } } else publishCompletedResult(Integer.parseInt("456" + id)); } public void publishCompletedResult(int id1) { try { mNotifyManager.cancel(id1); } catch (Exception e) { e.printStackTrace(); } } //check if copy is successful // avoid using the method as there is no way to know when we would be returning from command callbacks // rather confirm from the command result itself, inside it's callback boolean checkFiles(HFile hFile1, HFile hFile2) throws RootNotPermittedException { if (RootHelper.isDirectory(hFile1.getPath(), BaseActivity.rootMode, 5)) { if (RootHelper.fileExists(hFile2.getPath())) return false; ArrayList<BaseFile> baseFiles = RootHelper.getFilesList(hFile1.getPath(), true, true, null); if (baseFiles.size() > 0) { boolean b = true; for (BaseFile baseFile : baseFiles) { if (!checkFiles(new HFile(baseFile.getMode(), baseFile.getPath()), new HFile(hFile2.getMode(), hFile2.getPath() + "/" + (baseFile.getName())))) b = false; } return b; } return RootHelper.fileExists(hFile2.getPath()); } else { ArrayList<BaseFile> baseFiles = RootHelper.getFilesList(hFile1.getParent(), true, true, null); int i = -1; int index = -1; for (BaseFile b : baseFiles) { i++; if (b.getPath().equals(hFile1.getPath())) { index = i; break; } } ArrayList<BaseFile> baseFiles1 = RootHelper.getFilesList(hFile1.getParent(), true, true, null); int i1 = -1; int index1 = -1; for (BaseFile b : baseFiles1) { i1++; if (b.getPath().equals(hFile1.getPath())) { index1 = i1; break; } } return baseFiles.get(index).getSize() == baseFiles1.get(index1).getSize(); } } private BroadcastReceiver receiver3 = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //cancel operation progressHandler.setCancelled(true); } }; @Override public IBinder onBind(Intent arg0) { // TODO Auto-generated method stub return mBinder; } public class LocalBinder extends Binder { public CopyService getService() { // Return this instance of LocalService so clients can call public methods return CopyService.this; } } public interface ProgressListener { void onUpdate(DataPackage dataPackage); void refresh(); } public void setProgressListener(ProgressListener progressListener) { this.progressListener = progressListener; } /** * Returns the {@link #dataPackages} list which contains * data to be transferred to {@link com.amaze.filemanager.fragments.ProcessViewer} * Method call is synchronized so as to avoid modifying the list * by {@link ServiceWatcherUtil#handlerThread} while {@link MainActivity#runOnUiThread(Runnable)} * is executing the callbacks in {@link com.amaze.filemanager.fragments.ProcessViewer} * * @return */ public synchronized DataPackage getDataPackage(int index) { return this.dataPackages.get(index); } public synchronized int getDataPackageSize() { return this.dataPackages.size(); } /** * Puts a {@link DataPackage} into a list * Method call is synchronized so as to avoid modifying the list * by {@link ServiceWatcherUtil#handlerThread} while {@link MainActivity#runOnUiThread(Runnable)} * is executing the callbacks in {@link com.amaze.filemanager.fragments.ProcessViewer} * * @param dataPackage */ private synchronized void putDataPackage(DataPackage dataPackage) { this.dataPackages.add(dataPackage); } }