浏览代码

Merge pull request #12052 from nextcloud/feature/use-m3-file-downloader-notification

Use Material Design 3 File Downloader Notification
Andy Scherzinger 1 年之前
父节点
当前提交
3d5ee8a9df

+ 7 - 6
app/src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -24,14 +24,11 @@ package com.owncloud.android.datamodel;
 
 import android.content.ContentResolver;
 import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
 
-import com.nextcloud.android.common.ui.theme.utils.ColorRole;
 import com.owncloud.android.R;
 import com.owncloud.android.lib.common.network.WebdavEntry;
 import com.owncloud.android.lib.common.network.WebdavUtils;
@@ -41,9 +38,7 @@ import com.owncloud.android.lib.resources.files.model.GeoLocation;
 import com.owncloud.android.lib.resources.files.model.ImageDimension;
 import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
 import com.owncloud.android.lib.resources.shares.ShareeUser;
-import com.owncloud.android.utils.DrawableUtil;
 import com.owncloud.android.utils.MimeType;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -52,7 +47,6 @@ import java.util.List;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.core.content.ContextCompat;
 import androidx.core.content.FileProvider;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import third_parties.daveKoeller.AlphanumComparator;
@@ -350,6 +344,13 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         return false;
     }
 
+    public String getFileNameWithExtension(int fileNameLength) {
+        String fileName = getFileName();
+        String shortFileName = fileName.substring(0, Math.min(fileName.length(), fileNameLength));
+        String extension = "." + fileName.substring(fileName.lastIndexOf('.') + 1);
+        return shortFileName + extension;
+    }
+
     /**
      * The path, where the file is stored locally
      *

+ 0 - 741
app/src/main/java/com/owncloud/android/files/services/FileDownloader.java

@@ -1,741 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   Copyright (C) 2012 Bartek Przybylski
- *   Copyright (C) 2012-2016 ownCloud Inc.
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License version 2,
- *   as published by the Free Software Foundation.
- *
- *   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.owncloud.android.files.services;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.OnAccountsUpdateListener;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
-import android.graphics.BitmapFactory;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Pair;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.files.downloader.DownloadTask;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.R;
-import com.owncloud.android.authentication.AuthenticatorActivity;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.datamodel.UploadsStorageManager;
-import com.owncloud.android.lib.common.OwnCloudAccount;
-import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
-import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.files.FileUtils;
-import com.owncloud.android.operations.DownloadFileOperation;
-import com.owncloud.android.operations.DownloadType;
-import com.owncloud.android.providers.DocumentsStorageProvider;
-import com.owncloud.android.ui.activity.ConflictsResolveActivity;
-import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.dialog.SendShareDialog;
-import com.owncloud.android.ui.fragment.OCFileListFragment;
-import com.owncloud.android.ui.notifications.NotificationUtils;
-import com.owncloud.android.ui.preview.PreviewImageActivity;
-import com.owncloud.android.ui.preview.PreviewImageFragment;
-import com.owncloud.android.utils.ErrorMessageAdapter;
-import com.owncloud.android.utils.MimeTypeUtil;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.io.File;
-import java.security.SecureRandom;
-import java.util.AbstractList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Vector;
-
-import javax.inject.Inject;
-
-import androidx.core.app.NotificationCompat;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import dagger.android.AndroidInjection;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-public class FileDownloader extends Service
-        implements OnDatatransferProgressListener, OnAccountsUpdateListener {
-
-    public static final String EXTRA_USER = "USER";
-    public static final String EXTRA_FILE = "FILE";
-
-    private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED";
-    private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH";
-    public static final String EXTRA_DOWNLOAD_RESULT = "RESULT";
-    public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
-    public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
-    public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
-    public static final String DOWNLOAD_TYPE = "DOWNLOAD_TYPE";
-
-    private static final int FOREGROUND_SERVICE_ID = 412;
-
-    private static final String TAG = FileDownloader.class.getSimpleName();
-
-    private Looper mServiceLooper;
-    private ServiceHandler mServiceHandler;
-    private IBinder mBinder;
-    private OwnCloudClient mDownloadClient;
-    private Optional<User> currentUser = Optional.empty();
-    private FileDataStorageManager mStorageManager;
-
-    private IndexedForest<DownloadFileOperation> mPendingDownloads = new IndexedForest<>();
-
-    private DownloadFileOperation mCurrentDownload;
-
-    private NotificationManager mNotificationManager;
-    private NotificationCompat.Builder mNotificationBuilder;
-    private int mLastPercent;
-
-    private Notification mNotification;
-
-    private long conflictUploadId;
-
-    public boolean mStartedDownload = false;
-
-    @Inject UserAccountManager accountManager;
-    @Inject UploadsStorageManager uploadsStorageManager;
-    @Inject LocalBroadcastManager localBroadcastManager;
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    public static String getDownloadAddedMessage() {
-        return FileDownloader.class.getName() + DOWNLOAD_ADDED_MESSAGE;
-    }
-
-    public static String getDownloadFinishMessage() {
-        return FileDownloader.class.getName() + DOWNLOAD_FINISH_MESSAGE;
-    }
-
-    /**
-     * Service initialization
-     */
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        AndroidInjection.inject(this);
-        Log_OC.d(TAG, "Creating service");
-        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        HandlerThread thread = new HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-        mServiceLooper = thread.getLooper();
-        mServiceHandler = new ServiceHandler(mServiceLooper, this);
-        mBinder = new FileDownloaderBinder();
-
-        NotificationCompat.Builder builder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils).setContentTitle(
-            getApplicationContext().getResources().getString(R.string.app_name))
-            .setContentText(getApplicationContext().getResources().getString(R.string.foreground_service_download))
-            .setSmallIcon(R.drawable.notification_icon)
-            .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon));
-
-
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
-        }
-
-        mNotification = builder.build();
-
-        // add AccountsUpdatedListener
-        AccountManager am = AccountManager.get(getApplicationContext());
-        am.addOnAccountsUpdatedListener(this, null, false);
-    }
-
-
-    /**
-     * Service clean up
-     */
-    @Override
-    public void onDestroy() {
-        Log_OC.v(TAG, "Destroying service");
-        mBinder = null;
-        mServiceHandler = null;
-        mServiceLooper.quit();
-        mServiceLooper = null;
-        mNotificationManager = null;
-
-        // remove AccountsUpdatedListener
-        AccountManager am = AccountManager.get(getApplicationContext());
-        am.removeOnAccountsUpdatedListener(this);
-        super.onDestroy();
-    }
-
-
-    /**
-     * Entry point to add one or several files to the queue of downloads.
-     *
-     * New downloads are added calling to startService(), resulting in a call to this method.
-     * This ensures the service will keep on working although the caller activity goes away.
-     */
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        Log_OC.d(TAG, "Starting command with id " + startId);
-
-        startForeground(FOREGROUND_SERVICE_ID, mNotification);
-
-        if (intent == null || !intent.hasExtra(EXTRA_USER) || !intent.hasExtra(EXTRA_FILE)) {
-            Log_OC.e(TAG, "Not enough information provided in intent");
-            return START_NOT_STICKY;
-        } else {
-            final User user = intent.getParcelableExtra(EXTRA_USER);
-            final OCFile file = intent.getParcelableExtra(EXTRA_FILE);
-            final String behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
-
-            DownloadType downloadType = DownloadType.DOWNLOAD;
-            if (intent.hasExtra(DOWNLOAD_TYPE)) {
-                downloadType = (DownloadType) intent.getSerializableExtra(DOWNLOAD_TYPE);
-            }
-            String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
-            String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
-            conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1);
-            AbstractList<String> requestedDownloads = new Vector<String>();
-            try {
-                DownloadFileOperation newDownload = new DownloadFileOperation(user,
-                                                                              file,
-                                                                              behaviour,
-                                                                              activityName,
-                                                                              packageName,
-                                                                              getBaseContext(),
-                                                                              downloadType);
-                newDownload.addDatatransferProgressListener(this);
-                newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
-                Pair<String, String> putResult = mPendingDownloads.putIfAbsent(user.getAccountName(),
-                                                                               file.getRemotePath(),
-                                                                               newDownload);
-                if (putResult != null) {
-                    String downloadKey = putResult.first;
-                    requestedDownloads.add(downloadKey);
-                    sendBroadcastNewDownload(newDownload, putResult.second);
-                }   // else, file already in the queue of downloads; don't repeat the request
-
-            } catch (IllegalArgumentException e) {
-                Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
-                return START_NOT_STICKY;
-            }
-
-            if (requestedDownloads.size() > 0) {
-                Message msg = mServiceHandler.obtainMessage();
-                msg.arg1 = startId;
-                msg.obj = requestedDownloads;
-                mServiceHandler.sendMessage(msg);
-            }
-        }
-
-        return START_NOT_STICKY;
-    }
-
-    /**
-     * Provides a binder object that clients can use to perform operations on the queue of downloads,
-     * excepting the addition of new files.
-     *
-     * Implemented to perform cancellation, pause and resume of existing downloads.
-     */
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
-
-
-    /**
-     * Called when ALL the bound clients were onbound.
-     */
-    @Override
-    public boolean onUnbind(Intent intent) {
-        ((FileDownloaderBinder) mBinder).clearListeners();
-        return false;   // not accepting rebinding (default behaviour)
-    }
-
-    @Override
-    public void onAccountsUpdated(Account[] accounts) {
-         //review the current download and cancel it if its account doesn't exist
-        if (mCurrentDownload != null && !accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) {
-            mCurrentDownload.cancel();
-        }
-        // The rest of downloads are cancelled when they try to start
-    }
-
-
-    /**
-     * Binder to let client components to perform operations on the queue of downloads.
-     * <p/>
-     * It provides by itself the available operations.
-     */
-    public class FileDownloaderBinder extends Binder implements OnDatatransferProgressListener {
-
-        /**
-         * Map of listeners that will be reported about progress of downloads from a
-         * {@link FileDownloaderBinder}
-         * instance.
-         */
-        private Map<Long, OnDatatransferProgressListener> mBoundListeners =
-                new HashMap<Long, OnDatatransferProgressListener>();
-
-
-        /**
-         * Cancels a pending or current download of a remote file.
-         *
-         * @param account ownCloud account where the remote file is stored.
-         * @param file    A file in the queue of pending downloads
-         */
-        public void cancel(Account account, OCFile file) {
-            Pair<DownloadFileOperation, String> removeResult =
-                mPendingDownloads.remove(account.name, file.getRemotePath());
-            DownloadFileOperation download = removeResult.first;
-            if (download != null) {
-                download.cancel();
-            } else {
-                if (mCurrentDownload != null && currentUser.isPresent() &&
-                    mCurrentDownload.getRemotePath().startsWith(file.getRemotePath()) &&
-                        account.name.equals(currentUser.get().getAccountName())) {
-                    mCurrentDownload.cancel();
-                }
-            }
-        }
-
-        /**
-         * Cancels all the downloads for an account
-         */
-        public void cancel(String accountName) {
-            if (mCurrentDownload != null && mCurrentDownload.getUser().nameEquals(accountName)) {
-                mCurrentDownload.cancel();
-            }
-            // Cancel pending downloads
-            cancelPendingDownloads(accountName);
-        }
-
-        public void clearListeners() {
-            mBoundListeners.clear();
-        }
-
-
-        /**
-         * Returns True when the file described by 'file' in the ownCloud account 'account'
-         * is downloading or waiting to download.
-         *
-         * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or
-         * waiting to download.
-         *
-         * @param user    user where the remote file is stored.
-         * @param file    A file that could be in the queue of downloads.
-         */
-        public boolean isDownloading(User user, OCFile file) {
-            return user != null && file != null && mPendingDownloads.contains(user.getAccountName(), file.getRemotePath());
-        }
-
-
-        /**
-         * Adds a listener interested in the progress of the download for a concrete file.
-         *
-         * @param listener Object to notify about progress of transfer.
-         * @param file     {@link OCFile} of interest for listener.
-         */
-        public void addDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) {
-            if (file == null || listener == null) {
-                return;
-            }
-            mBoundListeners.put(file.getFileId(), listener);
-        }
-
-
-        /**
-         * Removes a listener interested in the progress of the download for a concrete file.
-         *
-         * @param listener      Object to notify about progress of transfer.
-         * @param file          {@link OCFile} of interest for listener.
-         */
-        public void removeDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) {
-            if (file == null || listener == null) {
-                return;
-            }
-            Long fileId = file.getFileId();
-            if (mBoundListeners.get(fileId) == listener) {
-                mBoundListeners.remove(fileId);
-            }
-        }
-
-        @Override
-        public void onTransferProgress(long progressRate, long totalTransferredSoFar,
-                                       long totalToTransfer, String fileName) {
-            OnDatatransferProgressListener boundListener =
-                    mBoundListeners.get(mCurrentDownload.getFile().getFileId());
-            if (boundListener != null) {
-                boundListener.onTransferProgress(progressRate, totalTransferredSoFar,
-                                                 totalToTransfer, fileName);
-            }
-        }
-
-    }
-
-    /**
-     * Download worker. Performs the pending downloads in the order they were requested.
-
-     * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
-     */
-    private static class ServiceHandler extends Handler {
-        // don't make it a final class, and don't remove the static ; lint will warn about a
-        // possible memory leak
-        FileDownloader mService;
-
-        public ServiceHandler(Looper looper, FileDownloader service) {
-            super(looper);
-            if (service == null) {
-                throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
-            }
-            mService = service;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            @SuppressWarnings("unchecked")
-            AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
-            if (msg.obj != null) {
-                Iterator<String> it = requestedDownloads.iterator();
-                while (it.hasNext()) {
-                    String next = it.next();
-                    mService.downloadFile(next);
-                }
-            }
-            mService.mStartedDownload=false;
-
-            (new Handler()).postDelayed(() -> {
-                if(!mService.mStartedDownload){
-                    mService.mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker);
-                }
-                Log_OC.d(TAG, "Stopping after command with id " + msg.arg1);
-                mService.mNotificationManager.cancel(FOREGROUND_SERVICE_ID);
-                mService.stopForeground(true);
-                mService.stopSelf(msg.arg1);
-            }, 2000);
-        }
-    }
-
-
-    /**
-     * Core download method: requests a file to download and stores it.
-     *
-     * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
-     */
-    private void downloadFile(String downloadKey) {
-
-        mStartedDownload = true;
-        mCurrentDownload = mPendingDownloads.get(downloadKey);
-
-        if (mCurrentDownload != null) {
-            // Detect if the account exists
-            if (accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) {
-                notifyDownloadStart(mCurrentDownload);
-                RemoteOperationResult downloadResult = null;
-                try {
-                    /// prepare client object to send the request to the ownCloud server
-                    Account currentDownloadAccount = mCurrentDownload.getUser().toPlatformAccount();
-                    Optional<User> currentDownloadUser = accountManager.getUser(currentDownloadAccount.name);
-                    if (!currentUser.equals(currentDownloadUser)) {
-                        currentUser = currentDownloadUser;
-                        mStorageManager = new FileDataStorageManager(currentUser.get(), getContentResolver());
-                    }   // else, reuse storage manager from previous operation
-
-                    // always get client from client manager, to get fresh credentials in case
-                    // of update
-                    OwnCloudAccount ocAccount = currentDownloadUser.get().toOwnCloudAccount();
-                    mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
-                            getClientFor(ocAccount, this);
-
-
-                    /// perform the download
-                    downloadResult = mCurrentDownload.execute(mDownloadClient);
-                    if (downloadResult.isSuccess() && mCurrentDownload.getDownloadType() == DownloadType.DOWNLOAD) {
-                        saveDownloadedFile();
-                    }
-
-                } catch (Exception e) {
-                    Log_OC.e(TAG, "Error downloading", e);
-                    downloadResult = new RemoteOperationResult(e);
-
-                } finally {
-                    Pair<DownloadFileOperation, String> removeResult = mPendingDownloads.removePayload(
-                        mCurrentDownload.getUser().getAccountName(), mCurrentDownload.getRemotePath());
-
-                    if (downloadResult == null) {
-                        downloadResult = new RemoteOperationResult(new RuntimeException("Error downloading…"));
-                    }
-
-                    /// notify result
-                    notifyDownloadResult(mCurrentDownload, downloadResult);
-                    sendBroadcastDownloadFinished(mCurrentDownload, downloadResult, removeResult.second);
-                }
-            } else {
-                cancelPendingDownloads(mCurrentDownload.getUser().getAccountName());
-            }
-        }
-    }
-
-
-    /**
-     * Updates the OC File after a successful download.
-     *
-     * TODO move to DownloadFileOperation
-     *  unify with code from {@link DocumentsStorageProvider} and {@link DownloadTask}.
-     */
-    private void saveDownloadedFile() {
-        OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
-
-        if (file == null) {
-            // try to get file via path, needed for overwriting existing files on conflict dialog
-            file = mStorageManager.getFileByDecryptedRemotePath(mCurrentDownload.getFile().getRemotePath());
-        }
-
-        if (file == null) {
-            Log_OC.e(this, "Could not save " + mCurrentDownload.getFile().getRemotePath());
-            return;
-        }
-
-        long syncDate = System.currentTimeMillis();
-        file.setLastSyncDateForProperties(syncDate);
-        file.setLastSyncDateForData(syncDate);
-        file.setUpdateThumbnailNeeded(true);
-        file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp());
-        file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp());
-        file.setEtag(mCurrentDownload.getEtag());
-        file.setMimeType(mCurrentDownload.getMimeType());
-        file.setStoragePath(mCurrentDownload.getSavePath());
-        file.setFileLength(new File(mCurrentDownload.getSavePath()).length());
-        file.setRemoteId(mCurrentDownload.getFile().getRemoteId());
-        mStorageManager.saveFile(file);
-        if (MimeTypeUtil.isMedia(mCurrentDownload.getMimeType())) {
-            FileDataStorageManager.triggerMediaScan(file.getStoragePath(), file);
-        }
-        mStorageManager.saveConflict(file, null);
-    }
-
-    /**
-     * Creates a status notification to show the download progress
-     *
-     * @param download Download operation starting.
-     */
-    private void notifyDownloadStart(DownloadFileOperation download) {
-        /// create status notification with a progress bar
-        mLastPercent = 0;
-        mNotificationBuilder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils);
-        mNotificationBuilder
-            .setSmallIcon(R.drawable.notification_icon)
-            .setTicker(getString(R.string.downloader_download_in_progress_ticker))
-            .setContentTitle(getString(R.string.downloader_download_in_progress_ticker))
-            .setOngoing(true)
-            .setProgress(100, 0, download.getSize() < 0)
-            .setContentText(
-                String.format(getString(R.string.downloader_download_in_progress_content), 0,
-                              new File(download.getSavePath()).getName())
-                           );
-
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
-        }
-
-        /// includes a pending intent in the notification showing the details view of the file
-        Intent showDetailsIntent = null;
-        if (PreviewImageFragment.canBePreviewed(download.getFile())) {
-            showDetailsIntent = new Intent(this, PreviewImageActivity.class);
-        } else {
-            showDetailsIntent = new Intent(this, FileDisplayActivity.class);
-        }
-        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.getFile());
-        showDetailsIntent.putExtra(FileActivity.EXTRA_USER, download.getUser());
-        showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
-        mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(),
-                                                                        showDetailsIntent, PendingIntent.FLAG_IMMUTABLE));
-
-
-        if (mNotificationManager == null) {
-            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        }
-        if (mNotificationManager != null) {
-            mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build());
-        }
-    }
-
-
-    /**
-     * Callback method to update the progress bar in the status notification.
-     */
-    @Override
-    public void onTransferProgress(long progressRate, long totalTransferredSoFar,
-                                   long totalToTransfer, String filePath) {
-        int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer));
-        if (percent != mLastPercent) {
-            mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0);
-            String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1);
-            String text = String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName);
-            mNotificationBuilder.setContentText(text);
-
-            if (mNotificationManager == null) {
-                mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-            }
-
-            if (mNotificationManager != null) {
-                mNotificationManager.notify(R.string.downloader_download_in_progress_ticker,
-                        mNotificationBuilder.build());
-            }
-        }
-        mLastPercent = percent;
-    }
-
-
-    /**
-     * Updates the status notification with the result of a download operation.
-     *
-     * @param downloadResult Result of the download operation.
-     * @param download       Finished download operation
-     */
-    @SuppressFBWarnings("DMI")
-    private void notifyDownloadResult(DownloadFileOperation download,
-                                      RemoteOperationResult downloadResult) {
-        if (mNotificationManager == null) {
-            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        }
-
-        if (!downloadResult.isCancelled()) {
-            if (downloadResult.isSuccess()) {
-                if (conflictUploadId > 0) {
-                    uploadsStorageManager.removeUpload(conflictUploadId);
-                }
-                // Dont show notification except an error has occured.
-                return;
-            }
-            int tickerId = downloadResult.isSuccess() ?
-                    R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
-
-            boolean needsToUpdateCredentials = ResultCode.UNAUTHORIZED == downloadResult.getCode();
-            tickerId = needsToUpdateCredentials ?
-                    R.string.downloader_download_failed_credentials_error : tickerId;
-
-            mNotificationBuilder
-                    .setTicker(getString(tickerId))
-                    .setContentTitle(getString(tickerId))
-                    .setAutoCancel(true)
-                    .setOngoing(false)
-                    .setProgress(0, 0, false);
-
-            if (needsToUpdateCredentials) {
-                configureUpdateCredentialsNotification(download.getUser());
-
-            } else {
-                // TODO put something smart in showDetailsIntent
-                Intent showDetailsIntent = new Intent();
-                mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(),
-                                                                                showDetailsIntent, PendingIntent.FLAG_IMMUTABLE));
-            }
-
-            mNotificationBuilder.setContentText(ErrorMessageAdapter.getErrorCauseMessage(downloadResult,
-                    download, getResources()));
-
-            if (mNotificationManager != null) {
-                mNotificationManager.notify((new SecureRandom()).nextInt(), mNotificationBuilder.build());
-
-                // Remove success notification
-                if (downloadResult.isSuccess()) {
-                    // Sleep 2 seconds, so show the notification before remove it
-                    NotificationUtils.cancelWithDelay(mNotificationManager,
-                                                      R.string.downloader_download_succeeded_ticker, 2000);
-                }
-            }
-        }
-    }
-
-    private void configureUpdateCredentialsNotification(User user) {
-        // let the user update credentials with one click
-        Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
-        updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount());
-        updateAccountCredentials.putExtra(
-                AuthenticatorActivity.EXTRA_ACTION,
-                AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
-        );
-        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-        updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
-        mNotificationBuilder.setContentIntent(
-            PendingIntent.getActivity(this,
-                                      (int) System.currentTimeMillis(),
-                                      updateAccountCredentials,
-                                      PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE)
-                                             );
-    }
-
-
-    /**
-     * Sends a broadcast when a download finishes in order to the interested activities can
-     * update their view
-     *
-     * @param download               Finished download operation
-     * @param downloadResult         Result of the download operation
-     * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from
-     */
-    private void sendBroadcastDownloadFinished(
-            DownloadFileOperation download,
-            RemoteOperationResult downloadResult,
-            String unlinkedFromRemotePath) {
-
-        Intent end = new Intent(getDownloadFinishMessage());
-        end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
-        end.putExtra(ACCOUNT_NAME, download.getUser().getAccountName());
-        end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
-        end.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.getBehaviour());
-        end.putExtra(SendShareDialog.ACTIVITY_NAME, download.getActivityName());
-        end.putExtra(SendShareDialog.PACKAGE_NAME, download.getPackageName());
-        if (unlinkedFromRemotePath != null) {
-            end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath);
-        }
-        end.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(end);
-    }
-
-
-    /**
-     * Sends a broadcast when a new download is added to the queue.
-     *
-     * @param download           Added download operation
-     * @param linkedToRemotePath Path in the downloads tree where the download was linked to
-     */
-    private void sendBroadcastNewDownload(DownloadFileOperation download,
-                                          String linkedToRemotePath) {
-        Intent added = new Intent(getDownloadAddedMessage());
-        added.putExtra(ACCOUNT_NAME, download.getUser().getAccountName());
-        added.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
-        added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath);
-        added.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(added);
-    }
-
-    private void cancelPendingDownloads(String accountName) {
-        mPendingDownloads.remove(accountName);
-    }
-}

+ 734 - 0
app/src/main/java/com/owncloud/android/files/services/FileDownloader.kt

@@ -0,0 +1,734 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   Copyright (C) 2012 Bartek Przybylski
+ *   Copyright (C) 2012-2016 ownCloud Inc.
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
+ *
+ *   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.owncloud.android.files.services
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.accounts.OnAccountsUpdateListener
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.IBinder
+import android.os.Looper
+import android.os.Message
+import android.os.Process
+import androidx.core.app.NotificationCompat
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.java.util.Optional
+import com.owncloud.android.R
+import com.owncloud.android.authentication.AuthenticatorActivity
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.DownloadFileOperation
+import com.owncloud.android.operations.DownloadType
+import com.owncloud.android.ui.activity.ConflictsResolveActivity
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.dialog.SendShareDialog
+import com.owncloud.android.ui.fragment.OCFileListFragment
+import com.owncloud.android.ui.notifications.NotificationUtils
+import com.owncloud.android.ui.preview.PreviewImageActivity
+import com.owncloud.android.ui.preview.PreviewImageFragment
+import com.owncloud.android.utils.ErrorMessageAdapter
+import com.owncloud.android.utils.MimeTypeUtil
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import dagger.android.AndroidInjection
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
+import java.io.File
+import java.security.SecureRandom
+import java.util.AbstractList
+import java.util.Vector
+import javax.inject.Inject
+
+class FileDownloader : Service(), OnDatatransferProgressListener, OnAccountsUpdateListener {
+    private var mServiceLooper: Looper? = null
+    private var mServiceHandler: ServiceHandler? = null
+    private var mBinder: IBinder? = null
+    private var mDownloadClient: OwnCloudClient? = null
+    private var currentUser = Optional.empty<User>()
+    private var mStorageManager: FileDataStorageManager? = null
+    private val mPendingDownloads = IndexedForest<DownloadFileOperation>()
+    private var mCurrentDownload: DownloadFileOperation? = null
+    private var notificationManager: NotificationManager? = null
+    private var notification: Notification? = null
+    private var notificationBuilder: NotificationCompat.Builder? = null
+    private var mLastPercent = 0
+    private var conflictUploadId: Long = 0
+    var mStartedDownload = false
+
+    @JvmField
+    @Inject
+    var accountManager: UserAccountManager? = null
+
+    @JvmField
+    @Inject
+    var uploadsStorageManager: UploadsStorageManager? = null
+
+    @JvmField
+    @Inject
+    var localBroadcastManager: LocalBroadcastManager? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    /**
+     * Service initialization
+     */
+    override fun onCreate() {
+        super.onCreate()
+
+        AndroidInjection.inject(this)
+        Log_OC.d(TAG, "Creating service")
+        initNotificationManager()
+        val thread = HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        mServiceLooper = thread.looper
+        mServiceHandler = ServiceHandler(mServiceLooper, this)
+        mBinder = FileDownloaderBinder()
+        initNotificationBuilder()
+
+        // add AccountsUpdatedListener
+        val am = AccountManager.get(applicationContext)
+        am.addOnAccountsUpdatedListener(this, null, false)
+    }
+
+    private fun initNotificationManager() {
+        if (notificationManager == null) {
+            notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+        }
+    }
+
+    private fun initNotificationBuilder() {
+        val resources = applicationContext.resources
+        val title = resources.getString(R.string.foreground_service_download)
+
+        notificationBuilder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils)
+            .setSmallIcon(R.drawable.notification_icon)
+            .setOngoing(true)
+            .setContentTitle(title)
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            notificationBuilder?.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
+        }
+        notification = notificationBuilder?.build()
+    }
+
+    private fun notifyNotificationManager() {
+        notificationManager?.notify(R.string.downloader_download_in_progress_ticker, notificationBuilder?.build())
+    }
+
+    /**
+     * Service clean up
+     */
+    override fun onDestroy() {
+        Log_OC.v(TAG, "Destroying service")
+        mBinder = null
+        mServiceHandler = null
+        mServiceLooper!!.quit()
+        mServiceLooper = null
+        notificationManager = null
+        notification = null
+        notificationBuilder = null
+
+        // remove AccountsUpdatedListener
+        val am = AccountManager.get(applicationContext)
+        am.removeOnAccountsUpdatedListener(this)
+        super.onDestroy()
+    }
+
+    /**
+     * Entry point to add one or several files to the queue of downloads.
+     *
+     * New downloads are added calling to startService(), resulting in a call to this method.
+     * This ensures the service will keep on working although the caller activity goes away.
+     */
+    @Suppress("LongParameterList")
+    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+        Log_OC.d(TAG, "Starting command with id $startId")
+
+        startForeground(FOREGROUND_SERVICE_ID, notification)
+
+        if (!intent.hasExtra(EXTRA_USER) || !intent.hasExtra(EXTRA_FILE)) {
+            Log_OC.e(TAG, "Not enough information provided in intent")
+            return START_NOT_STICKY
+        } else {
+            val user = intent.getParcelableExtra<User>(EXTRA_USER)
+            val file = intent.getParcelableExtra<OCFile>(EXTRA_FILE)
+            val behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR)
+            var downloadType: DownloadType? = DownloadType.DOWNLOAD
+            if (intent.hasExtra(DOWNLOAD_TYPE)) {
+                downloadType = intent.getSerializableExtra(DOWNLOAD_TYPE) as DownloadType?
+            }
+            val activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME)
+            val packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME)
+            conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1)
+            val requestedDownloads: AbstractList<String> = Vector()
+            try {
+                val newDownload = DownloadFileOperation(
+                    user,
+                    file,
+                    behaviour,
+                    activityName,
+                    packageName,
+                    baseContext,
+                    downloadType
+                )
+                newDownload.addDatatransferProgressListener(this)
+                newDownload.addDatatransferProgressListener(mBinder as FileDownloaderBinder?)
+                val putResult = mPendingDownloads.putIfAbsent(
+                    user!!.accountName,
+                    file!!.remotePath,
+                    newDownload
+                )
+                if (putResult != null) {
+                    val downloadKey = putResult.first
+                    requestedDownloads.add(downloadKey)
+                    sendBroadcastNewDownload(newDownload, putResult.second)
+                } // else, file already in the queue of downloads; don't repeat the request
+            } catch (e: IllegalArgumentException) {
+                Log_OC.e(TAG, "Not enough information provided in intent: " + e.message)
+                return START_NOT_STICKY
+            }
+            if (requestedDownloads.size > 0) {
+                val msg = mServiceHandler!!.obtainMessage()
+                msg.arg1 = startId
+                msg.obj = requestedDownloads
+                mServiceHandler!!.sendMessage(msg)
+            }
+        }
+        return START_NOT_STICKY
+    }
+
+    /**
+     * Provides a binder object that clients can use to perform operations on the queue of downloads,
+     * excepting the addition of new files.
+     *
+     * Implemented to perform cancellation, pause and resume of existing downloads.
+     */
+    override fun onBind(intent: Intent): IBinder? {
+        return mBinder
+    }
+
+    /**
+     * Called when ALL the bound clients were onbound.
+     */
+    override fun onUnbind(intent: Intent): Boolean {
+        (mBinder as FileDownloaderBinder?)!!.clearListeners()
+        return false // not accepting rebinding (default behaviour)
+    }
+
+    override fun onAccountsUpdated(accounts: Array<Account>) {
+        // review the current download and cancel it if its account doesn't exist
+        if (mCurrentDownload != null && !accountManager!!.exists(mCurrentDownload!!.user.toPlatformAccount())) {
+            mCurrentDownload!!.cancel()
+        }
+        // The rest of downloads are cancelled when they try to start
+    }
+
+    /**
+     * Binder to let client components to perform operations on the queue of downloads.
+     *
+     *
+     * It provides by itself the available operations.
+     */
+    inner class FileDownloaderBinder : Binder(), OnDatatransferProgressListener {
+        /**
+         * Map of listeners that will be reported about progress of downloads from a
+         * [FileDownloaderBinder]
+         * instance.
+         */
+        private val mBoundListeners: MutableMap<Long, OnDatatransferProgressListener> = HashMap()
+
+        /**
+         * Cancels a pending or current download of a remote file.
+         *
+         * @param account ownCloud account where the remote file is stored.
+         * @param file    A file in the queue of pending downloads
+         */
+        @Suppress("ComplexMethod")
+        fun cancel(account: Account, file: OCFile) {
+            val removeResult = mPendingDownloads.remove(account.name, file.remotePath)
+            val download = removeResult.first
+
+            if (download != null) {
+                download.cancel()
+            } else {
+                if (mCurrentDownload != null && currentUser.isPresent &&
+                    mCurrentDownload!!
+                        .remotePath
+                        .startsWith(file.remotePath) && account.name == currentUser.get().accountName
+                ) {
+                    mCurrentDownload!!.cancel()
+                }
+            }
+        }
+
+        /**
+         * Cancels all the downloads for an account
+         */
+        fun cancel(accountName: String?) {
+            if (mCurrentDownload != null && mCurrentDownload!!.user.nameEquals(accountName)) {
+                mCurrentDownload!!.cancel()
+            }
+            // Cancel pending downloads
+            cancelPendingDownloads(accountName)
+        }
+
+        fun clearListeners() {
+            mBoundListeners.clear()
+        }
+
+        /**
+         * Returns True when the file described by 'file' in the ownCloud account 'account'
+         * is downloading or waiting to download.
+         *
+         * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or
+         * waiting to download.
+         *
+         * @param user    user where the remote file is stored.
+         * @param file    A file that could be in the queue of downloads.
+         */
+        fun isDownloading(user: User?, file: OCFile?): Boolean {
+            return user != null && file != null && mPendingDownloads.contains(user.accountName, file.remotePath)
+        }
+
+        /**
+         * Adds a listener interested in the progress of the download for a concrete file.
+         *
+         * @param listener Object to notify about progress of transfer.
+         * @param file     [OCFile] of interest for listener.
+         */
+        fun addDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
+            if (file == null || listener == null) {
+                return
+            }
+            mBoundListeners[file.fileId] = listener
+        }
+
+        /**
+         * Removes a listener interested in the progress of the download for a concrete file.
+         *
+         * @param listener      Object to notify about progress of transfer.
+         * @param file          [OCFile] of interest for listener.
+         */
+        fun removeDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
+            if (file == null || listener == null) {
+                return
+            }
+            val fileId = file.fileId
+            if (mBoundListeners[fileId] === listener) {
+                mBoundListeners.remove(fileId)
+            }
+        }
+
+        override fun onTransferProgress(
+            progressRate: Long,
+            totalTransferredSoFar: Long,
+            totalToTransfer: Long,
+            fileName: String
+        ) {
+            val boundListener = mBoundListeners[mCurrentDownload!!.file.fileId]
+            boundListener?.onTransferProgress(
+                progressRate,
+                totalTransferredSoFar,
+                totalToTransfer,
+                fileName
+            )
+        }
+    }
+
+    /**
+     * Download worker. Performs the pending downloads in the order they were requested.
+     *
+     * Created with the Looper of a new thread, started in [FileUploader.onCreate].
+     */
+    private class ServiceHandler(looper: Looper?, service: FileDownloader?) : Handler(looper!!) {
+        // don't make it a final class, and don't remove the static ; lint will warn about a
+        // possible memory leak
+        var mService: FileDownloader
+
+        init {
+            requireNotNull(service) { "Received invalid NULL in parameter 'service'" }
+            mService = service
+        }
+
+        @Suppress("MagicNumber")
+        override fun handleMessage(msg: Message) {
+            val requestedDownloads = msg.obj as AbstractList<String>
+            if (msg.obj != null) {
+                val it: Iterator<String> = requestedDownloads.iterator()
+                while (it.hasNext()) {
+                    val next = it.next()
+                    mService.downloadFile(next)
+                }
+            }
+            mService.mStartedDownload = false
+
+            Handler(Looper.getMainLooper()).postDelayed({
+                if (!mService.mStartedDownload) {
+                    mService.notificationManager!!.cancel(R.string.downloader_download_in_progress_ticker)
+                }
+                Log_OC.d(TAG, "Stopping after command with id " + msg.arg1)
+                mService.notificationManager!!.cancel(FOREGROUND_SERVICE_ID)
+                mService.stopForeground(true)
+                mService.stopSelf(msg.arg1)
+            }, 2000)
+        }
+    }
+
+    /**
+     * Core download method: requests a file to download and stores it.
+     *
+     * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
+     */
+    @Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
+    private fun downloadFile(downloadKey: String) {
+        mStartedDownload = true
+        mCurrentDownload = mPendingDownloads[downloadKey]
+
+        if (mCurrentDownload != null) {
+            val isAccountExist = accountManager?.exists(mCurrentDownload!!.user.toPlatformAccount())
+
+            if (isAccountExist == true) {
+                notifyDownloadStart(mCurrentDownload!!)
+                var downloadResult: RemoteOperationResult<*>? = null
+                try {
+                    // / prepare client object to send the request to the ownCloud server
+                    val currentDownloadAccount = mCurrentDownload!!.user.toPlatformAccount()
+                    val currentDownloadUser = accountManager!!.getUser(currentDownloadAccount.name)
+                    if (currentUser != currentDownloadUser) {
+                        currentUser = currentDownloadUser
+                        mStorageManager = FileDataStorageManager(currentUser.get(), contentResolver)
+                    } // else, reuse storage manager from previous operation
+
+                    // always get client from client manager, to get fresh credentials in case
+                    // of update
+                    val ocAccount = currentDownloadUser.get().toOwnCloudAccount()
+                    mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this)
+
+                    // / perform the download
+                    downloadResult = mCurrentDownload!!.execute(mDownloadClient)
+                    if (downloadResult.isSuccess && mCurrentDownload!!.downloadType === DownloadType.DOWNLOAD) {
+                        saveDownloadedFile()
+                    }
+                } catch (e: Exception) {
+                    Log_OC.e(TAG, "Error downloading", e)
+                    downloadResult = RemoteOperationResult<Any?>(e)
+                } finally {
+                    val removeResult = mPendingDownloads.removePayload(
+                        mCurrentDownload!!.user.accountName,
+                        mCurrentDownload!!.remotePath
+                    )
+                    if (downloadResult == null) {
+                        downloadResult = RemoteOperationResult<Any?>(RuntimeException("Error downloading…"))
+                    }
+
+                    // / notify result
+                    notifyDownloadResult(mCurrentDownload!!, downloadResult)
+                    sendBroadcastDownloadFinished(mCurrentDownload!!, downloadResult, removeResult.second)
+                }
+            } else {
+                cancelPendingDownloads(mCurrentDownload!!.user.accountName)
+            }
+        }
+    }
+
+    /**
+     * Updates the OC File after a successful download.
+     *
+     * TODO move to DownloadFileOperation
+     * unify with code from [DocumentsStorageProvider] and [DownloadTask].
+     */
+    private fun saveDownloadedFile() {
+        var file = mStorageManager?.getFileById(mCurrentDownload!!.file.fileId)
+        if (file == null) {
+            // try to get file via path, needed for overwriting existing files on conflict dialog
+            file = mStorageManager?.getFileByDecryptedRemotePath(mCurrentDownload!!.file.remotePath)
+        }
+        if (file == null) {
+            Log_OC.e(this, "Could not save " + mCurrentDownload!!.file.remotePath)
+            return
+        }
+
+        val syncDate = System.currentTimeMillis()
+        file.lastSyncDateForProperties = syncDate
+        file.lastSyncDateForData = syncDate
+        file.isUpdateThumbnailNeeded = true
+        file.modificationTimestamp = mCurrentDownload!!.modificationTimestamp
+        file.modificationTimestampAtLastSyncForData = mCurrentDownload!!.modificationTimestamp
+        file.etag = mCurrentDownload!!.etag
+        file.mimeType = mCurrentDownload!!.mimeType
+        file.storagePath = mCurrentDownload!!.savePath
+        file.fileLength = File(mCurrentDownload!!.savePath).length()
+        file.remoteId = mCurrentDownload!!.file.remoteId
+        mStorageManager!!.saveFile(file)
+        if (MimeTypeUtil.isMedia(mCurrentDownload!!.mimeType)) {
+            FileDataStorageManager.triggerMediaScan(file.storagePath, file)
+        }
+        mStorageManager!!.saveConflict(file, null)
+    }
+
+    /**
+     * Creates a status notification to show the download progress
+     *
+     * @param download Download operation starting.
+     */
+    @Suppress("MagicNumber")
+    private fun notifyDownloadStart(download: DownloadFileOperation) {
+        val fileName = download.file.getFileNameWithExtension(10)
+        val titlePrefix = getString(R.string.file_downloader_notification_title_prefix)
+        val title = titlePrefix + fileName
+
+        // / update status notification with a progress bar
+        mLastPercent = 0
+        notificationBuilder
+            ?.setContentTitle(title)
+            ?.setTicker(title)
+            ?.setProgress(100, 0, download.size < 0)
+
+        // / includes a pending intent in the notification showing the details view of the file
+        val showDetailsIntent: Intent = if (PreviewImageFragment.canBePreviewed(download.file)) {
+            Intent(this, PreviewImageActivity::class.java)
+        } else {
+            Intent(this, FileDisplayActivity::class.java)
+        }
+
+        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.file)
+        showDetailsIntent.putExtra(FileActivity.EXTRA_USER, download.user)
+        showDetailsIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+        notificationBuilder?.setContentIntent(
+            PendingIntent.getActivity(
+                this,
+                System.currentTimeMillis().toInt(),
+                showDetailsIntent,
+                PendingIntent.FLAG_IMMUTABLE
+            )
+        )
+        initNotificationManager()
+        notifyNotificationManager()
+    }
+
+    /**
+     * Callback method to update the progress bar in the status notification.
+     */
+    @Suppress("MagicNumber")
+    override fun onTransferProgress(
+        progressRate: Long,
+        totalTransferredSoFar: Long,
+        totalToTransfer: Long,
+        filePath: String
+    ) {
+        val percent = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()
+        if (percent != mLastPercent) {
+            notificationBuilder?.setProgress(100, percent, totalToTransfer < 0)
+            initNotificationManager()
+            notifyNotificationManager()
+        }
+        mLastPercent = percent
+    }
+
+    /**
+     * Updates the status notification with the result of a download operation.
+     *
+     * @param downloadResult Result of the download operation.
+     * @param download       Finished download operation
+     */
+    @SuppressFBWarnings("DMI")
+    @Suppress("MagicNumber")
+    private fun notifyDownloadResult(
+        download: DownloadFileOperation,
+        downloadResult: RemoteOperationResult<*>
+    ) {
+        initNotificationManager()
+        if (!downloadResult.isCancelled) {
+            if (downloadResult.isSuccess) {
+                if (conflictUploadId > 0) {
+                    uploadsStorageManager!!.removeUpload(conflictUploadId)
+                }
+                // Don't show notification except an error has occurred.
+                return
+            }
+
+            var tickerId = if (downloadResult.isSuccess) {
+                R.string.downloader_download_succeeded_ticker
+            } else {
+                R.string.downloader_download_failed_ticker
+            }
+
+            val needsToUpdateCredentials = ResultCode.UNAUTHORIZED == downloadResult.code
+
+            tickerId = if (needsToUpdateCredentials) {
+                R.string.downloader_download_failed_credentials_error
+            } else {
+                tickerId
+            }
+
+            notificationBuilder
+                ?.setSmallIcon(R.drawable.notification_icon)
+                ?.setTicker(getString(tickerId))
+                ?.setAutoCancel(true)
+                ?.setOngoing(false)
+                ?.setProgress(0, 0, false)
+
+            if (needsToUpdateCredentials) {
+                configureUpdateCredentialsNotification(download.user)
+            } else {
+                // TODO put something smart in showDetailsIntent
+                val showDetailsIntent = Intent()
+                notificationBuilder?.setContentIntent(
+                    PendingIntent.getActivity(
+                        this,
+                        System.currentTimeMillis().toInt(),
+                        showDetailsIntent,
+                        PendingIntent.FLAG_IMMUTABLE
+                    )
+                )
+            }
+            notificationBuilder?.setContentText(
+                ErrorMessageAdapter.getErrorCauseMessage(
+                    downloadResult,
+                    download,
+                    resources
+                )
+            )
+            if (notificationManager != null) {
+                notificationManager?.notify(SecureRandom().nextInt(), notificationBuilder?.build())
+
+                // Remove success notification
+                if (downloadResult.isSuccess) {
+                    // Sleep 2 seconds, so show the notification before remove it
+                    NotificationUtils.cancelWithDelay(
+                        notificationManager,
+                        R.string.downloader_download_succeeded_ticker,
+                        2000
+                    )
+                }
+            }
+        }
+    }
+
+    private fun configureUpdateCredentialsNotification(user: User) {
+        // let the user update credentials with one click
+        val updateAccountCredentials = Intent(this, AuthenticatorActivity::class.java)
+        updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount())
+        updateAccountCredentials.putExtra(
+            AuthenticatorActivity.EXTRA_ACTION,
+            AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
+        )
+        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+        updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND)
+        notificationBuilder!!.setContentIntent(
+            PendingIntent.getActivity(
+                this,
+                System.currentTimeMillis().toInt(),
+                updateAccountCredentials,
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+            )
+        )
+    }
+
+    /**
+     * Sends a broadcast when a download finishes in order to the interested activities can
+     * update their view
+     *
+     * @param download               Finished download operation
+     * @param downloadResult         Result of the download operation
+     * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from
+     */
+    private fun sendBroadcastDownloadFinished(
+        download: DownloadFileOperation,
+        downloadResult: RemoteOperationResult<*>,
+        unlinkedFromRemotePath: String?
+    ) {
+        val end = Intent(downloadFinishMessage)
+        end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess)
+        end.putExtra(ACCOUNT_NAME, download.user.accountName)
+        end.putExtra(EXTRA_REMOTE_PATH, download.remotePath)
+        end.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour)
+        end.putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName)
+        end.putExtra(SendShareDialog.PACKAGE_NAME, download.packageName)
+        if (unlinkedFromRemotePath != null) {
+            end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
+        }
+        end.setPackage(packageName)
+        localBroadcastManager!!.sendBroadcast(end)
+    }
+
+    /**
+     * Sends a broadcast when a new download is added to the queue.
+     *
+     * @param download           Added download operation
+     * @param linkedToRemotePath Path in the downloads tree where the download was linked to
+     */
+    private fun sendBroadcastNewDownload(
+        download: DownloadFileOperation,
+        linkedToRemotePath: String
+    ) {
+        val added = Intent(downloadAddedMessage)
+        added.putExtra(ACCOUNT_NAME, download.user.accountName)
+        added.putExtra(EXTRA_REMOTE_PATH, download.remotePath)
+        added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath)
+        added.setPackage(packageName)
+        localBroadcastManager!!.sendBroadcast(added)
+    }
+
+    private fun cancelPendingDownloads(accountName: String?) {
+        mPendingDownloads.remove(accountName)
+    }
+
+    companion object {
+        const val EXTRA_USER = "USER"
+        const val EXTRA_FILE = "FILE"
+        private const val DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED"
+        private const val DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH"
+        const val EXTRA_DOWNLOAD_RESULT = "RESULT"
+        const val EXTRA_REMOTE_PATH = "REMOTE_PATH"
+        const val EXTRA_LINKED_TO_PATH = "LINKED_TO"
+        const val ACCOUNT_NAME = "ACCOUNT_NAME"
+        const val DOWNLOAD_TYPE = "DOWNLOAD_TYPE"
+        private const val FOREGROUND_SERVICE_ID = 412
+        private val TAG = FileDownloader::class.java.simpleName
+
+        @JvmStatic
+        val downloadAddedMessage: String
+            get() = FileDownloader::class.java.name + DOWNLOAD_ADDED_MESSAGE
+
+        @JvmStatic
+        val downloadFinishMessage: String
+            get() = FileDownloader::class.java.name + DOWNLOAD_FINISH_MESSAGE
+    }
+}

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -694,7 +694,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
         if (progressListener != null) {
             if (containerActivity.getFileDownloaderBinder() != null) {
                 containerActivity.getFileDownloaderBinder().
-                        addDatatransferProgressListener(progressListener, getFile());
+                        addDataTransferProgressListener(progressListener, getFile());
             }
             if (containerActivity.getFileUploaderBinder() != null) {
                 containerActivity.getFileUploaderBinder().
@@ -709,7 +709,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
         if (progressListener != null) {
             if (containerActivity.getFileDownloaderBinder() != null) {
                 containerActivity.getFileDownloaderBinder().
-                        removeDatatransferProgressListener(progressListener, getFile());
+                    removeDataTransferProgressListener(progressListener, getFile());
             }
             if (containerActivity.getFileUploaderBinder() != null) {
                 containerActivity.getFileUploaderBinder().

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java

@@ -261,7 +261,7 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
 
     public void listenForTransferProgress() {
         if (mProgressListener != null && !mListening && containerActivity.getFileDownloaderBinder() != null) {
-            containerActivity.getFileDownloaderBinder().addDatatransferProgressListener(mProgressListener, getFile());
+            containerActivity.getFileDownloaderBinder().addDataTransferProgressListener(mProgressListener, getFile());
             mListening = true;
             setButtonsForTransferring();
         }
@@ -271,7 +271,7 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
     public void leaveTransferProgress() {
         if (mProgressListener != null && containerActivity.getFileDownloaderBinder() != null) {
             containerActivity.getFileDownloaderBinder()
-                .removeDatatransferProgressListener(mProgressListener, getFile());
+                .removeDataTransferProgressListener(mProgressListener, getFile());
             mListening = false;
         }
     }

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -162,6 +162,7 @@
     <string name="uploads_view_upload_status_fetching_server_version">Fetching server version…</string>
     <string name="uploads_view_later_waiting_to_upload">Waiting to upload</string>
     <string name="uploads_view_group_header" translatable="false">%1$s (%2$d)</string>
+    <string name="file_downloader_notification_title_prefix">Downloading \u0020</string>
     <string name="downloader_download_in_progress_ticker">Downloading…</string>
     <string name="downloader_download_in_progress_content">%1$d%% Downloading %2$s</string>
     <string name="downloader_download_succeeded_ticker">Downloaded</string>