diff --git a/drawable_resources/alert-octagon.svg b/drawable_resources/alert-octagon.svg new file mode 100644 index 000000000000..47476ee27919 --- /dev/null +++ b/drawable_resources/alert-octagon.svg @@ -0,0 +1 @@ + diff --git a/scripts/lint/lint-results.txt b/scripts/lint/lint-results.txt index 4d1eaf39a016..eaacc19c5e75 100644 --- a/scripts/lint/lint-results.txt +++ b/scripts/lint/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 104 warnings + Lint Report: 103 warnings diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 74bddddb6acc..a197673da878 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -80,6 +80,7 @@ tools:ignore="UnusedAttribute"> @@ -88,6 +89,11 @@ + + + + diff --git a/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index fc6005590ee7..3291a4ce12ca 100644 --- a/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -22,7 +22,6 @@ package com.owncloud.android.datamodel; import android.accounts.Account; -import android.accounts.AccountManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; @@ -773,12 +772,14 @@ public static class AvatarGenerationTask extends AsyncTask(avatarGenerationListener); mCallContext = callContext; if (storageManager == null) { @@ -787,6 +788,9 @@ public AvatarGenerationTask(AvatarGenerationListener avatarGenerationListener, O mAccount = account; mResources = resources; mAvatarRadius = avatarRadius; + mUserId = userId; + mServerName = serverName; + mContext = context; } @SuppressFBWarnings("Dm") @@ -796,20 +800,17 @@ protected Drawable doInBackground(String... params) { try { if (mAccount != null) { - OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, - MainApp.getAppContext()); - mClient = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, MainApp.getAppContext()); + OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, mContext); + mClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, mContext); } - mUsername = params[0]; thumbnail = doAvatarInBackground(); } catch(OutOfMemoryError oome) { Log_OC.e(TAG, "Out of memory"); } catch(Throwable t){ // the app should never break due to a problem with avatars - Log_OC.e(TAG, "Generation of avatar for " + mUsername + " failed", t); + Log_OC.e(TAG, "Generation of avatar for " + mUserId + " failed", t); } return thumbnail; @@ -820,7 +821,7 @@ protected void onPostExecute(Drawable drawable) { AvatarGenerationListener listener = mAvatarGenerationListener.get(); AvatarGenerationTask avatarWorkerTask = getAvatarWorkerTask(mCallContext); - if (this == avatarWorkerTask && listener.shouldCallGeneratedCallback(mUsername, mCallContext)) { + if (this == avatarWorkerTask && listener.shouldCallGeneratedCallback(mUserId, mCallContext)) { listener.avatarGenerated(drawable, mCallContext); } } @@ -830,7 +831,7 @@ protected void onPostExecute(Drawable drawable) { * Converts size of file icon from dp to pixel * @return int */ - private int getAvatarDimension(){ + private int getAvatarDimension() { // Converts dp to pixel Resources r = MainApp.getAppContext().getResources(); return Math.round(r.getDimension(R.dimen.file_avatar_size)); @@ -839,13 +840,14 @@ private int getAvatarDimension(){ private @Nullable Drawable doAvatarInBackground() { Bitmap avatar = null; - String username = mUsername; - ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider( - MainApp.getAppContext().getContentResolver()); + String accountName = mUserId + "@" + mServerName; + + ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(mContext.getContentResolver()); + + String eTag = arbitraryDataProvider.getValue(accountName, ThumbnailsCacheManager.AVATAR); + String avatarKey = "a_" + mUserId + "_" + mServerName + "_" + eTag; - String eTag = arbitraryDataProvider.getValue(mAccount, AVATAR); - final String imageKey = "a_" + username + "_" + eTag; int px = getAvatarDimension(); // Download avatar from server @@ -854,18 +856,13 @@ Drawable doAvatarInBackground() { if (serverOCVersion.supportsRemoteThumbnails()) { GetMethod get = null; try { - String userId = AccountManager.get(MainApp.getAppContext()).getUserData(mAccount, - com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); - - if (TextUtils.isEmpty(userId)) { - userId = AccountUtils.getAccountUsername(username); - } - - String uri = mClient.getBaseUri() + "/index.php/avatar/" + Uri.encode(userId) + "/" + px; + String uri = mClient.getBaseUri() + "/index.php/avatar/" + Uri.encode(mUserId) + "/" + px; Log_OC.d("Avatar", "URI: " + uri); get = new GetMethod(uri); - if (!eTag.isEmpty()) { + // only use eTag if available and corresponding avatar is still there + // (might be deleted from cache) + if (!eTag.isEmpty() && getBitmapFromDiskCache(avatarKey) != null) { get.setRequestHeader("If-None-Match", eTag); } @@ -877,18 +874,19 @@ Drawable doAvatarInBackground() { // new avatar InputStream inputStream = get.getResponseBodyAsStream(); + String newETag = null; if (get.getResponseHeader(ETAG) != null) { - eTag = get.getResponseHeader(ETAG).getValue().replace("\"", ""); - arbitraryDataProvider.storeOrUpdateKeyValue(mAccount.name, AVATAR, eTag); + newETag = get.getResponseHeader(ETAG).getValue().replace("\"", ""); + arbitraryDataProvider.storeOrUpdateKeyValue(accountName, AVATAR, newETag); } Bitmap bitmap = BitmapFactory.decodeStream(inputStream); avatar = ThumbnailUtils.extractThumbnail(bitmap, px, px); // Add avatar to cache - if (avatar != null) { + if (avatar != null && !TextUtils.isEmpty(newETag)) { avatar = handlePNG(avatar, px, px); - String newImageKey = "a_" + username + "_" + eTag; + String newImageKey = "a_" + mUserId + "_" + mServerName + "_" + newETag; addBitmapToCache(newImageKey, avatar); } else { return TextDrawable.createAvatar(mAccount.name, mAvatarRadius); @@ -897,7 +895,7 @@ Drawable doAvatarInBackground() { case HttpStatus.SC_NOT_MODIFIED: // old avatar - avatar = getBitmapFromDiskCache(imageKey); + avatar = getBitmapFromDiskCache(avatarKey); mClient.exhaustResponse(get.getResponseBodyAsStream()); break; @@ -965,7 +963,7 @@ public static boolean cancelPotentialAvatarWork(Object file, ImageView imageView final AvatarGenerationTask avatarWorkerTask = getAvatarWorkerTask(imageView); if (avatarWorkerTask != null) { - final Object usernameData = avatarWorkerTask.mUsername; + final Object usernameData = avatarWorkerTask.mUserId; // If usernameData is not yet set or it differs from the new data if (usernameData == null || !usernameData.equals(file)) { // Cancel previous task @@ -984,7 +982,7 @@ public static boolean cancelPotentialAvatarWork(Object file, MenuItem menuItem) final AvatarGenerationTask avatarWorkerTask = getAvatarWorkerTask(menuItem); if (avatarWorkerTask != null) { - final Object usernameData = avatarWorkerTask.mUsername; + final Object usernameData = avatarWorkerTask.mUserId; // If usernameData is not yet set or it differs from the new data if (usernameData == null || !usernameData.equals(file)) { // Cancel previous task diff --git a/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/src/main/java/com/owncloud/android/files/FileMenuFilter.java index 0ef99cada9ce..2db72fa645d4 100644 --- a/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ b/src/main/java/com/owncloud/android/files/FileMenuFilter.java @@ -2,7 +2,9 @@ * ownCloud Android client application * * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -98,7 +100,6 @@ public FileMenuFilter(OCFile targetFile, Account account, ComponentsGetter cg, C public void filter(Menu menu, boolean inSingleFileFragment) { if (mFiles == null || mFiles.size() <= 0) { hideAll(menu); - } else { List toShow = new ArrayList<>(); List toHide = new ArrayList<>(); @@ -156,166 +157,187 @@ public static void hideMenuItems(MenuItem... items) { */ private void filter(List toShow, List toHide, boolean inSingleFileFragment) { boolean synchronizing = anyFileSynchronizing(); + OCCapability capability = mComponentsGetter.getStorageManager().getCapability(mAccount.name); + boolean endToEndEncryptionEnabled = capability != null && capability.getEndToEndEncryption().isTrue(); - /// decision is taken for each possible action on a file in the menu - - // DOWNLOAD - if (mFiles.isEmpty() || containsFolder() || anyFileDown() || synchronizing) { - toHide.add(R.id.action_download_file); + filterDownload(toShow, toHide, synchronizing); + filterRename(toShow, toHide, synchronizing); + filterMoveCopy(toShow, toHide, synchronizing); + filterRemove(toShow, toHide, synchronizing); + filterSelectAll(toShow, toHide, inSingleFileFragment); + filterDeselectAll(toShow, toHide, inSingleFileFragment); + filterOpenWith(toShow, toHide, synchronizing); + filterCancelSync(toShow, toHide, synchronizing); + filterSync(toShow, toHide, synchronizing); + filterShareFile(toShow, toHide, capability); + filterDetails(toShow, toHide); + filterKeepAvailableOffline(toShow, toHide, synchronizing); + filterDontKeepAvailableOffline(toShow, toHide, synchronizing); + filterFavorite(toShow, toHide, synchronizing); + filterUnfavorite(toShow, toHide, synchronizing); + filterEncrypt(toShow, toHide, endToEndEncryptionEnabled); + filterUnsetEncrypted(toShow, toHide, endToEndEncryptionEnabled); + filterSetPictureAs(toShow, toHide); + } + private void filterShareFile(List toShow, List toHide, OCCapability capability) { + if (containsEncryptedFile() || (!isShareViaLinkAllowed() && !isShareWithUsersAllowed()) || + !isSingleSelection() || !isShareApiEnabled(capability) || mOverflowMenu) { + toHide.add(R.id.action_send_share_file); } else { - toShow.add(R.id.action_download_file); + toShow.add(R.id.action_send_share_file); } + } - // RENAME - if (!isSingleSelection() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) { - toHide.add(R.id.action_rename_file); - + private void filterDetails(List toShow, List toHide) { + if (isSingleSelection()) { + toShow.add(R.id.action_see_details); } else { - toShow.add(R.id.action_rename_file); + toHide.add(R.id.action_see_details); } + } - // MOVE & COPY - if (mFiles.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) { - toHide.add(R.id.action_move); - toHide.add(R.id.action_copy); + private void filterKeepAvailableOffline(List toShow, List toHide, boolean synchronizing) { + if (!allFiles() || synchronizing || allKeptAvailableOffline()) { + toHide.add(R.id.action_keep_files_offline); } else { - toShow.add(R.id.action_move); - toShow.add(R.id.action_copy); + toShow.add(R.id.action_keep_files_offline); } + } - // REMOVE - if (mFiles.isEmpty() || synchronizing || containsEncryptedFolder()) { - toHide.add(R.id.action_remove_file); + private void filterDontKeepAvailableOffline(List toShow, List toHide, boolean synchronizing) { + if (!allFiles() || synchronizing || allNotKeptAvailableOffline()) { + toHide.add(R.id.action_unset_keep_files_offline); } else { - toShow.add(R.id.action_remove_file); + toShow.add(R.id.action_unset_keep_files_offline); } + } - // SELECT ALL - if (!inSingleFileFragment) { - // Show only if at least one item isn't selected. - if (mFiles.size() >= mNumberOfAllFiles || mOverflowMenu) { - toHide.add(R.id.action_select_all_action_menu); - } else { - toShow.add(R.id.action_select_all_action_menu); - } + private void filterFavorite(List toShow, List toHide, boolean synchronizing) { + if (mFiles.isEmpty() || synchronizing || allFavorites()) { + toHide.add(R.id.action_favorite); } else { - // Always hide in single file fragments - toHide.add(R.id.action_select_all_action_menu); + toShow.add(R.id.action_favorite); } + } - // DESELECT ALL - if (!inSingleFileFragment) { - // Show only if at least one item is selected. - if (mFiles.isEmpty() || mOverflowMenu) { - toHide.add(R.id.action_deselect_all_action_menu); - } else { - toShow.add(R.id.action_deselect_all_action_menu); - } - }else { - // Always hide in single file fragments - toHide.add(R.id.action_deselect_all_action_menu); + private void filterUnfavorite(List toShow, List toHide, boolean synchronizing) { + if (mFiles.isEmpty() || synchronizing || allNotFavorites()) { + toHide.add(R.id.action_unset_favorite); + } else { + toShow.add(R.id.action_unset_favorite); } + } - // OPEN WITH (different to preview!) - if (!isSingleFile() || !anyFileDown() || synchronizing) { - toHide.add(R.id.action_open_file_with); + private void filterEncrypt(List toShow, List toHide, boolean endToEndEncryptionEnabled) { + if (mFiles.isEmpty() || !isSingleSelection() || isSingleFile() || isEncryptedFolder() + || !endToEndEncryptionEnabled) { + toHide.add(R.id.action_encrypted); } else { - toShow.add(R.id.action_open_file_with); + toShow.add(R.id.action_encrypted); } + } - // CANCEL SYNCHRONIZATION - if (mFiles.isEmpty() || !synchronizing) { - toHide.add(R.id.action_cancel_sync); + private void filterUnsetEncrypted(List toShow, List toHide, boolean endToEndEncryptionEnabled) { + if (mFiles.isEmpty() || !isSingleSelection() || isSingleFile() || !isEncryptedFolder() + || !endToEndEncryptionEnabled) { + toHide.add(R.id.action_unset_encrypted); + } else { + toShow.add(R.id.action_unset_encrypted); + } + } + private void filterSetPictureAs(List toShow, List toHide) { + if (isSingleImage() && !MimeTypeUtil.isSVG(mFiles.iterator().next())) { + toShow.add(R.id.action_set_as_wallpaper); } else { - toShow.add(R.id.action_cancel_sync); + toHide.add(R.id.action_set_as_wallpaper); } + } - // SYNC CONTENTS (BOTH FILE AND FOLDER) + private void filterSync(List toShow, List toHide, boolean synchronizing) { if (mFiles.isEmpty() || (!anyFileDown() && !containsFolder()) || synchronizing) { toHide.add(R.id.action_sync_file); - } else { toShow.add(R.id.action_sync_file); } + } - // SHARE FILE - boolean shareViaLinkAllowed = (mContext != null && - mContext.getResources().getBoolean(R.bool.share_via_link_feature)); - boolean shareWithUsersAllowed = (mContext != null && - mContext.getResources().getBoolean(R.bool.share_with_users_feature)); - - OCCapability capability = mComponentsGetter.getStorageManager().getCapability(mAccount.name); - boolean shareApiEnabled = capability != null && - (capability.getFilesSharingApiEnabled().isTrue() || - capability.getFilesSharingApiEnabled().isUnknown() - ); - if (containsEncryptedFile() || (!shareViaLinkAllowed && !shareWithUsersAllowed) || - !isSingleSelection() || - !shareApiEnabled || mOverflowMenu) { - toHide.add(R.id.action_send_share_file); - } else { - toShow.add(R.id.action_send_share_file); - } - - // SEE DETAILS - if (!isSingleFile()) { - toHide.add(R.id.action_see_details); + private void filterCancelSync(List toShow, List toHide, boolean synchronizing) { + if (mFiles.isEmpty() || !synchronizing) { + toHide.add(R.id.action_cancel_sync); } else { - toShow.add(R.id.action_see_details); + toShow.add(R.id.action_cancel_sync); } + } - // Kept available offline - if (!allFiles() || synchronizing || allKeptAvailableOffline()) { - toHide.add(R.id.action_keep_files_offline); + private void filterOpenWith(List toShow, List toHide, boolean synchronizing) { + if (!isSingleFile() || !anyFileDown() || synchronizing) { + toHide.add(R.id.action_open_file_with); } else { - toShow.add(R.id.action_keep_files_offline); + toShow.add(R.id.action_open_file_with); } + } - // Not kept available offline - if (!allFiles() || synchronizing || allNotKeptAvailableOffline()) { - toHide.add(R.id.action_unset_keep_files_offline); + private void filterDeselectAll(List toShow, List toHide, boolean inSingleFileFragment) { + if (inSingleFileFragment) { + // Always hide in single file fragments + toHide.add(R.id.action_deselect_all_action_menu); } else { - toShow.add(R.id.action_unset_keep_files_offline); + // Show only if at least one item is selected. + if (mFiles.isEmpty() || mOverflowMenu) { + toHide.add(R.id.action_deselect_all_action_menu); + } else { + toShow.add(R.id.action_deselect_all_action_menu); + } } + } - // Favorite - if (mFiles.isEmpty() || synchronizing || allFavorites()) { - toHide.add(R.id.action_favorite); + private void filterSelectAll(List toShow, List toHide, boolean inSingleFileFragment) { + if (!inSingleFileFragment) { + // Show only if at least one item isn't selected. + if (mFiles.size() >= mNumberOfAllFiles || mOverflowMenu) { + toHide.add(R.id.action_select_all_action_menu); + } else { + toShow.add(R.id.action_select_all_action_menu); + } } else { - toShow.add(R.id.action_favorite); + // Always hide in single file fragments + toHide.add(R.id.action_select_all_action_menu); } + } - // Unfavorite - if (mFiles.isEmpty() || synchronizing || allNotFavorites()) { - toHide.add(R.id.action_unset_favorite); + private void filterRemove(List toShow, List toHide, boolean synchronizing) { + if (mFiles.isEmpty() || synchronizing || containsEncryptedFolder()) { + toHide.add(R.id.action_remove_file); } else { - toShow.add(R.id.action_unset_favorite); + toShow.add(R.id.action_remove_file); } + } - // Encryption - boolean endToEndEncryptionEnabled = capability != null && capability.getEndToEndEncryption().isTrue(); - if (mFiles.isEmpty() || !isSingleSelection() || isSingleFile() || isEncryptedFolder() - || !endToEndEncryptionEnabled) { - toHide.add(R.id.action_encrypted); + private void filterMoveCopy(List toShow, List toHide, boolean synchronizing) { + if (mFiles.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) { + toHide.add(R.id.action_move); + toHide.add(R.id.action_copy); } else { - toShow.add(R.id.action_encrypted); + toShow.add(R.id.action_move); + toShow.add(R.id.action_copy); } + } - // Un-encrypt - if (mFiles.isEmpty() || !isSingleSelection() || isSingleFile() || !isEncryptedFolder() - || !endToEndEncryptionEnabled) { - toHide.add(R.id.action_unset_encrypted); + private void filterRename(List toShow, List toHide, boolean synchronizing) { + if (!isSingleSelection() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) { + toHide.add(R.id.action_rename_file); } else { - toShow.add(R.id.action_unset_encrypted); + toShow.add(R.id.action_rename_file); } + } - // SET PICTURE AS - if (isSingleImage() && !MimeTypeUtil.isSVG(mFiles.iterator().next())) { - toShow.add(R.id.action_set_as_wallpaper); + private void filterDownload(List toShow, List toHide, boolean synchronizing) { + if (mFiles.isEmpty() || containsFolder() || anyFileDown() || synchronizing) { + toHide.add(R.id.action_download_file); } else { - toHide.add(R.id.action_set_as_wallpaper); + toShow.add(R.id.action_download_file); } } @@ -364,6 +386,23 @@ private boolean anyFileUploading(FileUploaderBinder uploaderBinder) { return uploading; } + private boolean isShareApiEnabled(OCCapability capability) { + return capability != null && + (capability.getFilesSharingApiEnabled().isTrue() || + capability.getFilesSharingApiEnabled().isUnknown() + ); + } + + private boolean isShareWithUsersAllowed() { + return mContext != null && + mContext.getResources().getBoolean(R.bool.share_with_users_feature); + } + + private boolean isShareViaLinkAllowed() { + return mContext != null && + mContext.getResources().getBoolean(R.bool.share_via_link_feature); + } + private boolean isSingleSelection() { return mFiles.size() == SINGLE_SELECT_ITEMS; } diff --git a/src/main/java/com/owncloud/android/operations/UnshareOperation.java b/src/main/java/com/owncloud/android/operations/UnshareOperation.java index 585ff6e86578..a30e650f8b73 100644 --- a/src/main/java/com/owncloud/android/operations/UnshareOperation.java +++ b/src/main/java/com/owncloud/android/operations/UnshareOperation.java @@ -1,8 +1,10 @@ -/** +/* * ownCloud Android client application * * @author masensio + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -15,7 +17,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ package com.owncloud.android.operations; @@ -109,4 +110,8 @@ private boolean existsFile(OwnCloudClient client, String remotePath){ return result.isSuccess(); } + public ShareType getShareType() { + return mShareType; + } + } diff --git a/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java b/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java index c798d6d54db6..a1f3b0a52948 100644 --- a/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java +++ b/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java @@ -1,8 +1,10 @@ -/** +/* * ownCloud Android client application * * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -15,7 +17,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ package com.owncloud.android.operations; @@ -31,26 +32,26 @@ /** - * Updates an existing private share for a given file + * Updates an existing private share for a given file. */ - public class UpdateSharePermissionsOperation extends SyncOperation { private long mShareId; private int mPermissions; + private long mExpirationDateInMillis; private String mPath; /** * Constructor * - * @param shareId Private {@link OCShare} to update. Mandatory argument + * @param shareId Private {@link OCShare} to update. Mandatory argument */ public UpdateSharePermissionsOperation(long shareId) { mShareId = shareId; mPermissions = -1; + mExpirationDateInMillis = 0L; } - /** * Set permissions to update in private share. * @@ -61,6 +62,17 @@ public void setPermissions(int permissions) { mPermissions = permissions; } + /** + * Set expiration date to update private share. + * + * @param expirationDateInMillis Expiration date to set to the public link. + * A negative value clears the current expiration date. + * Zero value (start-of-epoch) results in no update done on + * the expiration date. + */ + public void setExpirationDate(long expirationDateInMillis) { + mExpirationDateInMillis = expirationDateInMillis; + } @Override protected RemoteOperationResult run(OwnCloudClient client) { @@ -69,18 +81,15 @@ protected RemoteOperationResult run(OwnCloudClient client) { if (share == null) { // TODO try to get remote share before failing? - return new RemoteOperationResult( - RemoteOperationResult.ResultCode.SHARE_NOT_FOUND - ); + return new RemoteOperationResult(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND); } mPath = share.getPath(); // Update remote share with password - UpdateRemoteShareOperation updateOp = new UpdateRemoteShareOperation( - share.getRemoteId() - ); + UpdateRemoteShareOperation updateOp = new UpdateRemoteShareOperation(share.getRemoteId()); updateOp.setPermissions(mPermissions); + updateOp.setExpirationDate(mExpirationDateInMillis); RemoteOperationResult result = updateOp.execute(client); if (result.isSuccess()) { diff --git a/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java b/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java index 802163068126..586a11e451a9 100644 --- a/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java @@ -161,7 +161,7 @@ private Cursor searchForUsersOrGroups(Uri uri) { userQuery, REQUESTED_PAGE, RESULTS_PER_PAGE ); RemoteOperationResult result = searchRequest.execute(account, getContext()); - List names = new ArrayList(); + List names = new ArrayList<>(); if (result.isSuccess()) { for (Object o : result.getData()) { // Get JSonObjects from response diff --git a/src/main/java/com/owncloud/android/services/OperationsService.java b/src/main/java/com/owncloud/android/services/OperationsService.java index e4fefed7562a..cbe3414a86ac 100644 --- a/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/src/main/java/com/owncloud/android/services/OperationsService.java @@ -1,7 +1,11 @@ -/** +/* * ownCloud Android client application * + * @author masensio + * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -14,7 +18,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ package com.owncloud.android.services; @@ -583,8 +586,10 @@ private void nextOperation() { } else if (shareId > 0) { operation = new UpdateSharePermissionsOperation(shareId); - int permissions = operationIntent.getIntExtra(EXTRA_SHARE_PERMISSIONS, 1); + int permissions = operationIntent.getIntExtra(EXTRA_SHARE_PERMISSIONS, -1); ((UpdateSharePermissionsOperation)operation).setPermissions(permissions); + long expirationDateInMillis = operationIntent.getLongExtra(EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, 0L); + ((UpdateSharePermissionsOperation)operation).setExpirationDate(expirationDateInMillis); } } else if (action.equals(ACTION_CREATE_SHARE_WITH_SHAREE)) { diff --git a/src/main/java/com/owncloud/android/ui/TextDrawable.java b/src/main/java/com/owncloud/android/ui/TextDrawable.java index 5e6fb6e49a1b..0b779e2c136f 100644 --- a/src/main/java/com/owncloud/android/ui/TextDrawable.java +++ b/src/main/java/com/owncloud/android/ui/TextDrawable.java @@ -1,19 +1,20 @@ -/** +/* * ownCloud Android client application * * @author Andy Scherzinger * @author Tobias Kaminsiky * Copyright (C) 2016 ownCloud Inc. - *

+ * Copyright (C) 2018 Andy Scherzinger + * * 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 . */ @@ -103,6 +104,23 @@ public static TextDrawable createAvatar(String accountName, float radiusInDp) th return createNamedAvatar(username, radiusInDp); } + /** + * creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the + * given radius. + * + * @param userId userId to use + * @param radiusInDp the circle's radius + * @return the avatar as a TextDrawable + * @throws UnsupportedEncodingException if the charset is not supported when calculating the color values + * @throws NoSuchAlgorithmException if the specified algorithm is not available when calculating the color values + */ + @NonNull + @NextcloudServer(max = 12) + public static TextDrawable createAvatarByUserId(String userId, float radiusInDp) throws + UnsupportedEncodingException, NoSuchAlgorithmException { + return createNamedAvatar(userId, radiusInDp); + } + /** * creates an avatar in form of a TextDrawable with the first letter of a name in a circle with the * given radius. @@ -130,7 +148,7 @@ public static TextDrawable createNamedAvatar(String name, float radiusInDp) thro * @param canvas The canvas to draw into */ @Override - public void draw(Canvas canvas) { + public void draw(@NonNull Canvas canvas) { canvas.drawCircle(mRadius, mRadius, mRadius, mBackground); canvas.drawText(mText, mRadius, mRadius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint); } diff --git a/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java b/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java index e11ab3252cf7..0959ad828735 100644 --- a/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java @@ -51,7 +51,6 @@ /** * This activity shows all settings for contact backup/restore */ - public class ContactsPreferenceActivity extends FileActivity implements FileFragment.ContainerActivity { public static final String TAG = ContactsPreferenceActivity.class.getSimpleName(); @@ -171,6 +170,11 @@ public void showDetails(OCFile file) { // not needed } + @Override + public void showDetails(OCFile file, int activeTab) { + // not needed + } + @Override public void onBrowsedDownTo(OCFile folder) { // not needed diff --git a/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index a99959452206..f13f509628de 100644 --- a/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -648,8 +648,8 @@ public void updateAccountList() { View accountEndView = findNavigationViewChildById(R.id.drawer_account_end); accountEndView.setTag(mAvatars[1].name); - DisplayUtils.setAvatar(mAvatars[1], this, - mOtherAccountAvatarRadiusDimension, getResources(), getStorageManager(), accountEndView); + DisplayUtils.setAvatar(mAvatars[1], this, mOtherAccountAvatarRadiusDimension, getResources(), + getStorageManager(), accountEndView, this); mAccountEndAccountAvatar.setVisibility(View.VISIBLE); } else { mAccountEndAccountAvatar.setVisibility(View.GONE); @@ -660,8 +660,8 @@ public void updateAccountList() { View accountMiddleView = findNavigationViewChildById(R.id.drawer_account_middle); accountMiddleView.setTag(mAvatars[2].name); - DisplayUtils.setAvatar(mAvatars[2], this, - mOtherAccountAvatarRadiusDimension, getResources(), getStorageManager(), accountMiddleView); + DisplayUtils.setAvatar(mAvatars[2], this, mOtherAccountAvatarRadiusDimension, getResources(), + getStorageManager(), accountMiddleView, this); mAccountMiddleAccountAvatar.setVisibility(View.VISIBLE); } else { mAccountMiddleAccountAvatar.setVisibility(View.GONE); @@ -695,7 +695,7 @@ private void repopulateAccountList(ArrayList accounts) { account.name) .setIcon(TextDrawable.createAvatar(account.name, mMenuAccountAvatarRadiusDimension)); DisplayUtils.setAvatar(account, this, mMenuAccountAvatarRadiusDimension, getResources(), - getStorageManager(), accountMenuItem); + getStorageManager(), accountMenuItem, this); } } catch (Exception e) { Log_OC.e(TAG, "Error calculating RGB value for account menu item.", e); @@ -761,7 +761,7 @@ protected void setAccountInDrawer(Account account) { currentAccountView.setTag(account.name); DisplayUtils.setAvatar(account, this, mCurrentAccountAvatarRadiusDimension, getResources(), - getStorageManager(), currentAccountView); + getStorageManager(), currentAccountView, this); // check and show quota info if available getAndDisplayUserQuota(); diff --git a/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index 4495bc201e09..d150f08e1260 100644 --- a/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -553,6 +553,7 @@ public FileUploaderBinder getFileUploaderBinder() { public void restart() { Intent i = new Intent(this, FileDisplayActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + i.setAction(FileDisplayActivity.RESTART); startActivity(i); fetchExternalLinks(false); diff --git a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index f532bf101045..1bd19493965e 100644 --- a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -3,8 +3,10 @@ * * @author Bartek Przybylski * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2011 Bartek Przybylski * Copyright (C) 2016 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -26,6 +28,7 @@ import android.accounts.AuthenticatorException; import android.annotation.TargetApi; import android.app.Activity; +import android.app.SearchManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -38,13 +41,16 @@ import android.content.SyncRequest; import android.content.pm.PackageManager; import android.content.res.Resources.NotFoundException; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.StringRes; import android.support.design.widget.BottomNavigationView; import android.support.design.widget.Snackbar; +import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; @@ -71,22 +77,33 @@ import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; +import com.owncloud.android.lib.common.OwnCloudAccount; +import com.owncloud.android.lib.common.accounts.AccountUtils; import com.owncloud.android.lib.common.operations.RemoteOperation; 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.shares.OCShare; +import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.media.MediaService; import com.owncloud.android.media.MediaServiceBinder; import com.owncloud.android.operations.CopyFileOperation; import com.owncloud.android.operations.CreateFolderOperation; +import com.owncloud.android.operations.CreateShareViaLinkOperation; +import com.owncloud.android.operations.CreateShareWithShareeOperation; import com.owncloud.android.operations.MoveFileOperation; import com.owncloud.android.operations.RefreshFolderOperation; import com.owncloud.android.operations.RemoveFileOperation; import com.owncloud.android.operations.RenameFileOperation; import com.owncloud.android.operations.SynchronizeFileOperation; +import com.owncloud.android.operations.UnshareOperation; +import com.owncloud.android.operations.UpdateSharePermissionsOperation; +import com.owncloud.android.operations.UpdateShareViaLinkOperation; import com.owncloud.android.operations.UploadFileOperation; +import com.owncloud.android.providers.UsersAndGroupsSearchProvider; import com.owncloud.android.syncadapter.FileSyncAdapter; import com.owncloud.android.ui.dialog.SendShareDialog; +import com.owncloud.android.ui.dialog.ShareLinkToDialog; import com.owncloud.android.ui.dialog.SortingOrderDialogFragment; import com.owncloud.android.ui.events.SyncEventFinished; import com.owncloud.android.ui.events.TokenPushEvent; @@ -131,6 +148,8 @@ public class FileDisplayActivity extends HookActivity implements FileFragment.ContainerActivity, OnEnforceableRefreshListener, SortingOrderDialogFragment.OnSortingOrderListener, SendShareDialog.SendShareDialogDownloader { + + public static final String RESTART = "RESTART"; private SyncBroadcastReceiver mSyncBroadcastReceiver; private UploadFinishReceiver mUploadFinishReceiver; @@ -141,6 +160,9 @@ public class FileDisplayActivity extends HookActivity private View mLeftFragmentContainer; private View mRightFragmentContainer; + private static final String TAG_PUBLIC_LINK = "PUBLIC_LINK"; + private static final String FTAG_CHOOSER_DIALOG = "CHOOSER_DIALOG"; + private static final String KEY_WAITING_TO_PREVIEW = "WAITING_TO_PREVIEW"; private static final String KEY_SYNC_IN_PROGRESS = "SYNC_IN_PROGRESS"; private static final String KEY_WAITING_TO_SEND = "WAITING_TO_SEND"; @@ -484,6 +506,65 @@ protected void onNewIntent(Intent intent) { if (intent.getAction() != null && intent.getAction().equalsIgnoreCase(ACTION_DETAILS)) { setIntent(intent); setFile(intent.getParcelableExtra(EXTRA_FILE)); + } else if (RESTART.equals(intent.getAction())) { + finish(); + startActivity(intent); + } else // Verify the action and get the query + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + String query = intent.getStringExtra(SearchManager.QUERY); + Log_OC.w(TAG, "Ignored Intent requesting to query for " + query); + + } else if (UsersAndGroupsSearchProvider.ACTION_SHARE_WITH.equals(intent.getAction())) { + Uri data = intent.getData(); + String dataString = intent.getDataString(); + String shareWith = dataString.substring(dataString.lastIndexOf('/') + 1); + + ArrayList shareeNames = new ArrayList<>(); + for (OCShare share : getStorageManager().getSharesWithForAFile(getFile().getRemotePath(), getAccount().name)) { + shareeNames.add(share.getShareWith()); + } + + if (!shareeNames.contains(shareWith)) { + doShareWith(shareWith, data.getAuthority()); + } + + } else { + Log_OC.e(TAG, "Unexpected intent " + intent.toString()); + } + } + + private void doShareWith(String shareeName, String dataAuthority) { + + ShareType shareType = UsersAndGroupsSearchProvider.getShareType(dataAuthority); + + getFileOperationsHelper().shareFileWithSharee( + getFile(), + shareeName, + shareType, + getAppropiatePermissions(shareType) + ); + } + + private int getAppropiatePermissions(ShareType shareType) { + + // check if the Share is FEDERATED + boolean isFederated = ShareType.FEDERATED.equals(shareType); + + if (getFile().isSharedWithMe()) { + return OCShare.READ_PERMISSION_FLAG; // minimum permissions + + } else if (isFederated) { + if (com.owncloud.android.authentication.AccountUtils + .getServerVersion(getAccount()).isNotReshareableFederatedSupported()) { + return (getFile().isFolder() ? OCShare.FEDERATED_PERMISSIONS_FOR_FOLDER_AFTER_OC9 : + OCShare.FEDERATED_PERMISSIONS_FOR_FILE_AFTER_OC9); + } else { + return (getFile().isFolder() ? OCShare.FEDERATED_PERMISSIONS_FOR_FOLDER_UP_TO_OC9 : + OCShare.FEDERATED_PERMISSIONS_FOR_FILE_UP_TO_OC9); + } + } else { + return (getFile().isFolder() ? OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER : + OCShare.MAXIMUM_PERMISSIONS_FOR_FILE); } } @@ -1488,7 +1569,18 @@ public void onBrowsedDownTo(OCFile directory) { */ @Override public void showDetails(OCFile file) { - Fragment detailFragment = FileDetailFragment.newInstance(file, getAccount()); + showDetails(file, 0); + } + + /** + * Shows the information of the {@link OCFile} received as a + * parameter in the second fragment. + * + * @param file {@link OCFile} whose details will be shown + * @param activeTab the active tab in the details view + */ + public void showDetails(OCFile file, int activeTab) { + Fragment detailFragment = FileDetailFragment.newInstance(file, getAccount(), activeTab); setSecondFragment(detailFragment); updateFragmentsVisibility(true); updateActionBarTitleAndHomeButton(file); @@ -1614,23 +1706,27 @@ public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationRe if (operation instanceof RemoveFileOperation) { onRemoveFileOperationFinish((RemoveFileOperation) operation, result); - } else if (operation instanceof RenameFileOperation) { onRenameFileOperationFinish((RenameFileOperation) operation, result); - } else if (operation instanceof SynchronizeFileOperation) { onSynchronizeFileOperationFinish((SynchronizeFileOperation) operation, result); - } else if (operation instanceof CreateFolderOperation) { onCreateFolderOperationFinish((CreateFolderOperation) operation, result); - } else if (operation instanceof MoveFileOperation) { onMoveFileOperationFinish((MoveFileOperation) operation, result); - } else if (operation instanceof CopyFileOperation) { onCopyFileOperationFinish((CopyFileOperation) operation, result); + } else if (operation instanceof CreateShareViaLinkOperation) { + onCreateShareViaLinkOperationFinish((CreateShareViaLinkOperation) operation, result); + } else if (operation instanceof CreateShareWithShareeOperation) { + onUpdateShareInformation(result, R.string.sharee_add_failed); + } else if (operation instanceof UpdateShareViaLinkOperation) { + onUpdateShareInformation(result, R.string.updating_share_failed); + } else if (operation instanceof UpdateSharePermissionsOperation) { + onUpdateShareInformation(result, R.string.updating_share_failed); + } else if (operation instanceof UnshareOperation) { + onUpdateShareInformation(result, R.string.unsharing_failed); } - } private void refreshShowDetails() { @@ -1723,6 +1819,117 @@ private void onMoveFileOperationFinish(MoveFileOperation operation, } } + private void onCreateShareViaLinkOperationFinish(CreateShareViaLinkOperation operation, + RemoteOperationResult result) { + FileDetailFragment fileDetailFragment = getShareFileFragment(); + if (result.isSuccess()) { + updateFileFromDB(); + + // Create dialog to allow the user choose an app to send the link + Intent intentToShareLink = new Intent(Intent.ACTION_SEND); + + // if share to user and share via link multiple ocshares are returned, + // therefore filtering for public_link + String link = ""; + for (Object object : result.getData()) { + OCShare shareLink = (OCShare) object; + if (TAG_PUBLIC_LINK.equalsIgnoreCase(shareLink.getShareType().name())) { + link = shareLink.getShareLink(); + break; + } + } + + intentToShareLink.putExtra(Intent.EXTRA_TEXT, link); + intentToShareLink.setType("text/plain"); + + String username; + try { + OwnCloudAccount oca = new OwnCloudAccount(getAccount(), this); + if (oca.getDisplayName() != null && !oca.getDisplayName().isEmpty()) { + username = oca.getDisplayName(); + } else { + username = AccountUtils.getUsernameForAccount(getAccount()); + } + } catch (Exception e) { + username = AccountUtils.getUsernameForAccount(getAccount()); + } + + if (username != null) { + intentToShareLink.putExtra( + Intent.EXTRA_SUBJECT, + getString( + R.string.subject_user_shared_with_you, + username, + getFile().getFileName() + ) + ); + } else { + intentToShareLink.putExtra( + Intent.EXTRA_SUBJECT, + getString( + R.string.subject_shared_with_you, + getFile().getFileName() + ) + ); + } + + String[] packagesToExclude = new String[]{getPackageName()}; + DialogFragment chooserDialog = ShareLinkToDialog.newInstance(intentToShareLink, packagesToExclude); + chooserDialog.show(getSupportFragmentManager(), FTAG_CHOOSER_DIALOG); + + fileDetailFragment.getFileDetailSharingFragment().refreshPublicShareFromDB(); + refreshListOfFilesFragment(false); + } else { + // Detect Failure (403) --> maybe needs password + String password = operation.getPassword(); + if (result.getCode() == RemoteOperationResult.ResultCode.SHARE_FORBIDDEN && + (password == null || password.length() == 0) && + getCapabilities().getFilesSharingPublicEnabled().isUnknown()) { + // Was tried without password, but not sure that it's optional. + + // Try with password before giving up; see also ShareFileFragment#OnShareViaLinkListener + if (fileDetailFragment != null + && fileDetailFragment.isAdded()) { // only if added to the view hierarchy!! + + fileDetailFragment.getFileDetailSharingFragment().requestPasswordForShareViaLink(true); + } + + } else { + fileDetailFragment.getFileDetailSharingFragment().refreshPublicShareFromDB(); + Snackbar.make( + findViewById(android.R.id.content), + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()), + Snackbar.LENGTH_LONG + ).show(); + } + } + } + + private void onUpdateShareInformation(RemoteOperationResult result, @StringRes int errorString) { + Fragment fileDetailFragment = getSecondFragment(); + + if (result.isSuccess()) { + updateFileFromDB(); + refreshListOfFilesFragment(false); + } else if (fileDetailFragment.getView() != null) { + Snackbar.make(fileDetailFragment.getView(), errorString, Snackbar.LENGTH_LONG).show(); + } + + if (fileDetailFragment != null && fileDetailFragment instanceof FileDetailFragment) { + ((FileDetailFragment) fileDetailFragment).getFileDetailSharingFragment() + .onUpdateShareInformation(result, getFile()); + } + } + + /** + * Shortcut to get access to the {@link FileDetailFragment} instance, if any + * + * @return A {@link FileDetailFragment} instance, or null + */ + private FileDetailFragment getShareFileFragment() { + return (FileDetailFragment) getSupportFragmentManager().findFragmentByTag(TAG_SECOND_FRAGMENT); + } + /** * Updates the view associated to the activity after the finish of an operation trying to copy a * file. diff --git a/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java b/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java index f7b5d7e7d2fb..4f91873e6351 100644 --- a/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java @@ -532,17 +532,16 @@ public void onReceive(Context context, Intent intent) { } } - /** - * Shows the information of the {@link OCFile} received as a - * parameter in the second fragment. - * - * @param file {@link OCFile} whose details will be shown - */ @Override public void showDetails(OCFile file) { // not used at the moment } + @Override + public void showDetails(OCFile file, int activeTab) { + // not used at the moment + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java b/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java index 509b92ba3156..2a111c92d9af 100644 --- a/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java @@ -22,12 +22,15 @@ package com.owncloud.android.ui.activity; +import android.graphics.Bitmap; import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.ColorInt; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; +import android.widget.ImageView; import android.widget.ProgressBar; import com.owncloud.android.R; @@ -40,6 +43,7 @@ */ public abstract class ToolbarActivity extends BaseActivity { private ProgressBar mProgressBar; + private ImageView mPreviewImage; @Override protected void onCreate(Bundle savedInstanceState) { @@ -66,6 +70,8 @@ protected void setupToolbar(boolean useBackgroundImage) { ThemeUtils.colorToolbarProgressBar(this, ThemeUtils.primaryColor(this)); } + mPreviewImage = findViewById(R.id.preview_image); + ThemeUtils.colorStatusBar(this, primaryDarkColor); if (toolbar.getOverflowIcon() != null) { @@ -81,7 +87,7 @@ protected void setupToolbar(boolean useBackgroundImage) { } } - protected void setupToolbar() { + public void setupToolbar() { setupToolbar(false); } @@ -146,7 +152,60 @@ public boolean isRoot(OCFile file) { * @param indeterminate true to enable the indeterminate mode */ public void setIndeterminate(boolean indeterminate) { - mProgressBar.setIndeterminate(indeterminate); + if (mProgressBar != null) { + mProgressBar.setIndeterminate(indeterminate); + } + } + + /** + * Change the visibility for the toolbar's progress bar. + * + * @param visibility visibility of the progress bar + */ + public void setProgressBarVisibility(int visibility) { + if (mProgressBar != null) { + mProgressBar.setVisibility(visibility); + } + } + + /** + * Change the visibility for the toolbar's preview image. + * + * @param visibility visibility of the preview image + */ + public void setPreviewImageVisibility(int visibility) { + if (mPreviewImage != null) { + mPreviewImage.setVisibility(visibility); + } + } + + /** + * Change the bitmap for the toolbar's preview image. + * + * @param bitmap bitmap of the preview image + */ + public void setPreviewImageBitmap(Bitmap bitmap) { + if (mPreviewImage != null) { + mPreviewImage.setImageBitmap(bitmap); + } + } + + /** + * Change the drawable for the toolbar's preview image. + * + * @param drawable drawable of the preview image + */ + public void setPreviewImageDrawable(Drawable drawable) { + if (mPreviewImage != null) { + mPreviewImage.setImageDrawable(drawable); + } + } + + /** + * get the toolbar's preview image view. + */ + public ImageView getPreviewImageView() { + return mPreviewImage; } /** diff --git a/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java b/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java index d2edb0795a1f..f30172c0d4dd 100644 --- a/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java @@ -263,7 +263,7 @@ private void populateUserInfoUi(UserInfo userInfo) { userName.setText(account.name); avatar.setTag(account.name); DisplayUtils.setAvatar(account, UserInfoActivity.this, mCurrentAccountAvatarRadiusDimension, getResources(), - getStorageManager(), avatar); + getStorageManager(), avatar, this); int tint = ThemeUtils.primaryColor(account, this); diff --git a/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java index 935faf4cc261..ec353d57aadf 100644 --- a/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java @@ -153,7 +153,7 @@ private void setAvatar(AccountViewHolderItem viewHolder, Account account) { View viewItem = viewHolder.imageViewItem; viewItem.setTag(account.name); DisplayUtils.setAvatar(account, this, mAccountAvatarRadiusDimension, mContext.getResources(), - mContext.getStorageManager(), viewItem); + mContext.getStorageManager(), viewItem, mContext); } catch (Exception e) { Log_OC.e(TAG, "Error calculating RGB value for account list item.", e); // use user icon as a fallback diff --git a/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java index 8b71890d99e3..11ae8ff74763 100644 --- a/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java @@ -36,6 +36,8 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter { private OCFile file; private Account account; + private FileDetailSharingFragment fileDetailSharingFragment; + public FileDetailTabAdapter(FragmentManager fm, OCFile file, Account account) { super(fm); @@ -49,12 +51,17 @@ public Fragment getItem(int position) { case 0: return FileDetailActivitiesFragment.newInstance(file, account); case 1: - return FileDetailSharingFragment.newInstance(file, account); + fileDetailSharingFragment = FileDetailSharingFragment.newInstance(file, account); + return fileDetailSharingFragment; default: return null; } } + public FileDetailSharingFragment getFileDetailSharingFragment() { + return fileDetailSharingFragment; + } + @Override public int getCount() { return 2; diff --git a/src/main/java/com/owncloud/android/ui/adapter/ShareUserListAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/ShareUserListAdapter.java index af726e9b4108..e637e72ef99f 100644 --- a/src/main/java/com/owncloud/android/ui/adapter/ShareUserListAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/ShareUserListAdapter.java @@ -1,4 +1,4 @@ -/** +/* * ownCloud Android client application * * @author masensio diff --git a/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java index 6d7d120fbede..5c80c2fc61e5 100644 --- a/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java @@ -1,113 +1,367 @@ -/** - * ownCloud Android client application - * - * @author masensio - * Copyright (C) 2015 ownCloud Inc. +/* + * Nextcloud Android client application * - * 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. + * @author masensio + * @author Andy Scherzinger + * Copyright (C) 2015 ownCloud GmbH + * Copyright (C) 2018 Andy Scherzinger * - * 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. + * 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. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * 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 . */ package com.owncloud.android.ui.adapter; +import android.accounts.Account; import android.content.Context; +import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; +import android.support.v4.app.FragmentManager; +import android.support.v7.widget.AppCompatCheckBox; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.ImageView; +import android.widget.PopupMenu; import android.widget.TextView; import com.owncloud.android.R; +import com.owncloud.android.authentication.AccountUtils; +import com.owncloud.android.datamodel.FileDataStorageManager; +import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.ShareType; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.ui.TextDrawable; +import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment; +import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper; +import com.owncloud.android.utils.DisplayUtils; +import com.owncloud.android.utils.ThemeUtils; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; /** - * Adapter to show a user/group in Share With List in Details View + * Adapter to show a user/group/email/remote in Sharing list in file details view. */ -public class UserListAdapter extends ArrayAdapter { +public class UserListAdapter extends RecyclerView.Adapter + implements DisplayUtils.AvatarGenerationListener { - private Context mContext; - private ArrayList mShares; - private float mAvatarRadiusDimension; + private ShareeListAdapterListener listener; + private OCCapability capabilities; + private FragmentManager fragmentManager; + private Context context; + private int accentColor; + private List shares; + private float avatarRadiusDimension; + private Account account; + private OCFile file; + private FileDataStorageManager storageManager; - public UserListAdapter(Context context, int resource, ArrayList shares) { - super(context, resource); - mContext = context; - mShares = shares; - - mAvatarRadiusDimension = context.getResources().getDimension(R.dimen.standard_padding); - } + public UserListAdapter(FragmentManager fragmentManager, Context context, List shares, Account account, + OCFile file, ShareeListAdapterListener listener) { + this.context = context; + this.fragmentManager = fragmentManager; + this.shares = shares; + this.listener = listener; + this.account = account; + this.file = file; - @Override - public int getCount() { - return mShares.size(); + accentColor = ThemeUtils.primaryAccentColor(context); + storageManager = new FileDataStorageManager(account, context.getContentResolver()); + capabilities = storageManager.getCapability(account.name); + avatarRadiusDimension = context.getResources().getDimension(R.dimen.user_icon_radius); } + @NonNull @Override - public Object getItem(int position) { - return mShares.get(position); + public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.file_details_share_user_item, parent, false); + return new UserViewHolder(v); } @Override - public long getItemId(int position) { - return 0; - } + public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { + if (shares != null && shares.size() > position) { + final OCShare share = shares.get(position); - @Override - public @NonNull View getView(final int position, View convertView, @NonNull ViewGroup parent) { - View view = convertView; - if (view == null) { - LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = inflater.inflate(R.layout.file_details_share_user_item, parent, false); - } - - if (mShares != null && mShares.size() > position) { - OCShare share = mShares.get(position); - - TextView userName = view.findViewById(R.id.userOrGroupName); - ImageView icon = view.findViewById(R.id.userIcon); String name = share.getSharedWithDisplayName(); if (share.getShareType() == ShareType.GROUP) { - name = getContext().getString(R.string.share_group_clarification, name); + name = context.getString(R.string.share_group_clarification, name); try { - icon.setImageDrawable(TextDrawable.createNamedAvatar(name, mAvatarRadiusDimension)); + holder.avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension)); } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { - icon.setImageResource(R.drawable.ic_group); + holder.avatar.setImageResource(R.drawable.ic_group); } } else if (share.getShareType() == ShareType.EMAIL) { - name = getContext().getString(R.string.share_email_clarification, name); + name = context.getString(R.string.share_email_clarification, name); try { - icon.setImageDrawable(TextDrawable.createNamedAvatar(name, mAvatarRadiusDimension)); + holder.avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension)); } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { - icon.setImageResource(R.drawable.ic_email); + holder.avatar.setImageResource(R.drawable.ic_email); } } else { - try { - icon.setImageDrawable(TextDrawable.createNamedAvatar(name, mAvatarRadiusDimension)); - } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { - icon.setImageResource(R.drawable.ic_user); + holder.avatar.setTag(share.getShareWith()); + DisplayUtils.setAvatar(account, share.getShareWith(), this, avatarRadiusDimension, + context.getResources(), storageManager, holder.avatar, context); + } + holder.name.setText(name); + + ThemeUtils.tintCheckbox(holder.allowEditing, accentColor); + holder.allowEditing.setChecked(canEdit(share)); + holder.allowEditing.setOnClickListener(v -> allowEditClick(holder.allowEditing, share)); + + // bind listener to edit privileges + holder.editShareButton.setOnClickListener(v -> onOverflowIconClicked(v, holder.allowEditing, share)); + } + } + + @Override + public long getItemId(int position) { + return shares.get(position).getId(); + } + + @Override + public int getItemCount() { + return shares.size(); + } + + private void allowEditClick(AppCompatCheckBox checkBox, @NonNull OCShare share) { + if (!share.isFolder()) { + share.setPermissions(listener.updatePermissionsToShare( + share, + canReshare(share), + checkBox.isChecked(), + false, + false, + false + )); + } else { + share.setPermissions(listener.updatePermissionsToShare( + share, + canReshare(share), + checkBox.isChecked(), + checkBox.isChecked(), + checkBox.isChecked(), + checkBox.isChecked() + )); + } + } + + private void onOverflowIconClicked(View view, AppCompatCheckBox allowEditsCheckBox, OCShare share) { + // use grey as fallback for elements where custom theming is not available + if (ThemeUtils.themingEnabled(context)) { + context.getTheme().applyStyle(R.style.FallbackThemingTheme, true); + } + PopupMenu popup = new PopupMenu(context, view); + popup.inflate(R.menu.file_detail_sharing_menu); + + prepareOptionsMenu(popup.getMenu(), share); + + popup.setOnMenuItemClickListener(item -> optionsItemSelected(popup.getMenu(), item, allowEditsCheckBox, share)); + popup.show(); + } + + /** + * Updates the sharee's menu with the current permissions of the {@link OCShare} + * + * @param menu the menu of the sharee/shared file + * @param share the shared file + */ + private void prepareOptionsMenu(Menu menu, OCShare share) { + + MenuItem reshareItem = menu.findItem(R.id.action_can_reshare); + if (isReshareForbidden(share)) { + reshareItem.setVisible(false); + } + reshareItem.setChecked(canReshare(share)); + + MenuItem editCreateItem = menu.findItem(R.id.action_can_edit_create); + MenuItem editChangeItem = menu.findItem(R.id.action_can_edit_change); + MenuItem editDeleteItem = menu.findItem(R.id.action_can_edit_delete); + if (file.isFolder() && isEditOptionsAvailable(share)) { + /// TODO change areEditOptionsAvailable in order to delete !isFederated + editCreateItem.setChecked(canCreate(share)); + editChangeItem.setChecked(canUpdate(share)); + editDeleteItem.setChecked(canDelete(share)); + } else { + editCreateItem.setVisible(false); + editChangeItem.setVisible(false); + editDeleteItem.setVisible(false); + } + + FileDetailSharingFragmentHelper.setupExpirationDateMenuItem( + menu.findItem(R.id.action_expiration_date), share.getExpirationDate(), context.getResources()); + } + + private boolean isEditOptionsAvailable(OCShare share) { + return !ShareType.FEDERATED.equals(share.getShareType()) + || AccountUtils.getServerVersion(account).isNotReshareableFederatedSupported(); + } + + private boolean isReshareForbidden(OCShare share) { + return ShareType.FEDERATED.equals(share.getShareType()) || + (capabilities != null && capabilities.getFilesSharingResharing().isFalse()); + } + + private boolean canEdit(OCShare share) { + return (share.getPermissions() & + (OCShare.CREATE_PERMISSION_FLAG | OCShare.UPDATE_PERMISSION_FLAG | OCShare.DELETE_PERMISSION_FLAG)) > 0; + } + + private boolean canCreate(OCShare share) { + return (share.getPermissions() & OCShare.CREATE_PERMISSION_FLAG) > 0; + } + + private boolean canUpdate(OCShare share) { + return (share.getPermissions() & OCShare.UPDATE_PERMISSION_FLAG) > 0; + } + + private boolean canDelete(OCShare share) { + return (share.getPermissions() & OCShare.DELETE_PERMISSION_FLAG) > 0; + } + + private boolean canReshare(OCShare share) { + return (share.getPermissions() & OCShare.SHARE_PERMISSION_FLAG) > 0; + } + + private boolean optionsItemSelected(Menu menu, MenuItem item, AppCompatCheckBox allowEditsCheckBox, OCShare share) { + switch (item.getItemId()) { + case R.id.action_can_edit_create: + case R.id.action_can_edit_change: + case R.id.action_can_edit_delete: { + item.setChecked(!item.isChecked()); + if (item.isChecked() && !allowEditsCheckBox.isChecked()) { + allowEditsCheckBox.setChecked(true); } + share.setPermissions( + updatePermissionsToShare( + share, + menu.findItem(R.id.action_can_reshare).isChecked(), + allowEditsCheckBox.isChecked(), + menu.findItem(R.id.action_can_edit_create).isChecked(), + menu.findItem(R.id.action_can_edit_change).isChecked(), + menu.findItem(R.id.action_can_edit_delete).isChecked()) + ); + return true; + } + case R.id.action_can_reshare: { + item.setChecked(!item.isChecked()); + share.setPermissions( + updatePermissionsToShare( + share, + menu.findItem(R.id.action_can_reshare).isChecked(), + allowEditsCheckBox.isChecked(), + menu.findItem(R.id.action_can_edit_create).isChecked(), + menu.findItem(R.id.action_can_edit_change).isChecked(), + menu.findItem(R.id.action_can_edit_delete).isChecked()) + ); + return true; } - userName.setText(name); + case R.id.action_unshare: { + listener.unshareWith(share); + shares.remove(share); + notifyDataSetChanged(); + return true; + } + case R.id.action_expiration_date: { + ExpirationDatePickerDialogFragment dialog = ExpirationDatePickerDialogFragment.newInstance(share, -1); + dialog.show( + fragmentManager, + ExpirationDatePickerDialogFragment.DATE_PICKER_DIALOG + ); + return true; + } + default: + return true; + } + } + + private int updatePermissionsToShare(OCShare share, boolean canReshare, boolean canEdit, boolean canEditCreate, + boolean canEditChange, boolean canEditDelete) { + return listener.updatePermissionsToShare( + share, + canReshare, + canEdit, + canEditCreate, + canEditChange, + canEditDelete + ); + } + + @Override + public void avatarGenerated(Drawable avatarDrawable, Object callContext) { + if (callContext instanceof ImageView) { + ImageView iv = (ImageView) callContext; + iv.setImageDrawable(avatarDrawable); + } + } + @Override + public boolean shouldCallGeneratedCallback(String tag, Object callContext) { + if (callContext instanceof ImageView) { + ImageView iv = (ImageView) callContext; + return String.valueOf(iv.getTag()).equals(tag); } - return view; + return false; + } + + class UserViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.avatar) + ImageView avatar; + @BindView(R.id.name) + TextView name; + @BindView(R.id.allowEditing) + AppCompatCheckBox allowEditing; + @BindView(R.id.editShareButton) + ImageView editShareButton; + + UserViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } + + public interface ShareeListAdapterListener { + /** + * unshare with given sharee {@link OCShare}. + * + * @param share the share + */ + void unshareWith(OCShare share); + + /** + * Updates the permissions of the {@link OCShare}. + * + * @param share the share to be updated + * @param canReshare reshare permission + * @param canEdit edit permission + * @param canEditCreate create permission (folders only) + * @param canEditChange change permission (folders only) + * @param canEditDelete delete permission (folders only) + * @return permissions value set + */ + int updatePermissionsToShare(OCShare share, + boolean canReshare, + boolean canEdit, + boolean canEditCreate, + boolean canEditChange, + boolean canEditDelete); } -} \ No newline at end of file +} diff --git a/src/main/java/com/owncloud/android/ui/decoration/SimpleListItemDividerDecoration.java b/src/main/java/com/owncloud/android/ui/decoration/SimpleListItemDividerDecoration.java new file mode 100644 index 000000000000..1d5d0306f280 --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/decoration/SimpleListItemDividerDecoration.java @@ -0,0 +1,78 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * Copyright (C) 2018 Andy Scherzinger + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + */ + +package com.owncloud.android.ui.decoration; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.RecyclerView; +import android.util.DisplayMetrics; +import android.view.View; + +/** + * DividerItemDecoration based on {@link DividerItemDecoration} adding a 72dp left padding. + */ +public class SimpleListItemDividerDecoration extends DividerItemDecoration { + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + + private final Rect bounds = new Rect(); + private Drawable divider; + private int leftPadding; + + /** + * Default divider will be used + */ + public SimpleListItemDividerDecoration(Context context) { + super(context, DividerItemDecoration.VERTICAL); + final TypedArray styledAttributes = context.obtainStyledAttributes(ATTRS); + divider = styledAttributes.getDrawable(0); + leftPadding = Math.round(72 * (context.getResources().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT)); + styledAttributes.recycle(); + } + + @Override + public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) { + canvas.save(); + final int right; + //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. + if (parent.getClipToPadding()) { + right = parent.getWidth() - parent.getPaddingRight(); + canvas.clipRect(leftPadding, parent.getPaddingTop(), right, + parent.getHeight() - parent.getPaddingBottom()); + } else { + right = parent.getWidth(); + } + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + parent.getDecoratedBoundsWithMargins(child, bounds); + final int bottom = bounds.bottom + Math.round(child.getTranslationY()); + final int top = bottom - 1; + divider.setBounds(leftPadding, top, right, bottom); + divider.draw(canvas); + } + canvas.restore(); + } +} diff --git a/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java b/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java index 03739fdb01a5..0244ebcd2064 100644 --- a/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java +++ b/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java @@ -1,8 +1,10 @@ -/** +/* * ownCloud Android client application * * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -15,7 +17,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ package com.owncloud.android.ui.dialog; @@ -24,11 +25,14 @@ import android.app.DatePickerDialog; import android.app.Dialog; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.text.format.DateUtils; import android.widget.DatePicker; +import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.ui.activity.FileActivity; import java.util.Calendar; @@ -46,18 +50,24 @@ public class ExpirationDatePickerDialogFragment /** Parameter constant for {@link OCFile} instance to set the expiration date */ private static final String ARG_FILE = "FILE"; + /** Parameter constant for {@link OCShare} instance to set the expiration date */ + private static final String ARG_SHARE = "SHARE"; + /** Parameter constant for date chosen initially */ private static final String ARG_CHOSEN_DATE_IN_MILLIS = "CHOSEN_DATE_IN_MILLIS"; /** File to bind an expiration date */ - private OCFile mFile; + private OCFile file; + + /** Share to bind an expiration date */ + private OCShare share; /** * Factory method to create new instances * * @param file File to bind an expiration date * @param chosenDateInMillis Date chosen when the dialog appears - * @return New dialog instance + * @return New dialog instance */ public static ExpirationDatePickerDialogFragment newInstance(OCFile file, long chosenDateInMillis) { Bundle arguments = new Bundle(); @@ -69,15 +79,34 @@ public static ExpirationDatePickerDialogFragment newInstance(OCFile file, long c return dialog; } + /** + * Factory method to create new instances + * + * @param share share to bind an expiration date + * @param chosenDateInMillis Date chosen when the dialog appears + * @return New dialog instance + */ + public static ExpirationDatePickerDialogFragment newInstance(OCShare share, long chosenDateInMillis) { + Bundle arguments = new Bundle(); + arguments.putParcelable(ARG_SHARE, share); + arguments.putLong(ARG_CHOSEN_DATE_IN_MILLIS, chosenDateInMillis); + + ExpirationDatePickerDialogFragment dialog = new ExpirationDatePickerDialogFragment(); + dialog.setArguments(arguments); + return dialog; + } + /** * {@inheritDoc} * - * @return A new dialog to let the user choose an expiration date that will be bound to a share link. + * @return A new dialog to let the user choose an expiration date that will be bound to a share link. */ @Override + @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { // Load arguments - mFile = getArguments().getParcelable(ARG_FILE); + file = getArguments().getParcelable(ARG_FILE); + share = getArguments().getParcelable(ARG_SHARE); // Chosen date received as an argument must be later than tomorrow ; default to tomorrow in other case final Calendar chosenDate = Calendar.getInstance(); @@ -92,11 +121,23 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { // Create a new instance of DatePickerDialog DatePickerDialog dialog = new DatePickerDialog( getActivity(), + R.style.FallbackDatePickerDialogTheme, this, chosenDate.get(Calendar.YEAR), chosenDate.get(Calendar.MONTH), chosenDate.get(Calendar.DAY_OF_MONTH) ); + dialog.setButton( + Dialog.BUTTON_NEUTRAL, + getText(R.string.share_via_link_unset_password), + (dialog1, which) -> { + if (file != null) { + ((FileActivity) getActivity()).getFileOperationsHelper() + .setExpirationDateToShareViaLink(file, -1); + } else if (share != null) { + ((FileActivity) getActivity()).getFileOperationsHelper().setExpirationDateToShare(share,-1); + } + }); // Prevent days in the past may be chosen DatePicker picker = dialog.getDatePicker(); @@ -127,9 +168,11 @@ public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth chosenDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); long chosenDateInMillis = chosenDate.getTimeInMillis(); - ((FileActivity)getActivity()).getFileOperationsHelper().setExpirationDateToShareViaLink( - mFile, - chosenDateInMillis - ); + if (file != null) { + ((FileActivity) getActivity()).getFileOperationsHelper() + .setExpirationDateToShareViaLink(file, chosenDateInMillis); + } else if (share != null) { + ((FileActivity) getActivity()).getFileOperationsHelper().setExpirationDateToShare(share,chosenDateInMillis); + } } } diff --git a/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.java b/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.java index 1ab9a4b1ad50..c050efa7eb39 100644 --- a/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.java +++ b/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.java @@ -16,11 +16,13 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.adapter.SendButtonAdapter; import com.owncloud.android.ui.components.SendButtonData; import com.owncloud.android.ui.helpers.FileOperationsHelper; @@ -34,8 +36,10 @@ * Nextcloud Android client application * * @author Tobias Kaminsky + * @author Andy Scherzinger * Copyright (C) 2017 Tobias Kaminsky * Copyright (C) 2017 Nextcloud GmbH. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -53,20 +57,23 @@ public class SendShareDialog extends BottomSheetDialogFragment { private static final String KEY_OCFILE = "KEY_OCFILE"; + private static final String KEY_HIDE_NCSHARING_OPTIONS = "KEY_HIDE_NCSHARING_OPTIONS"; private static final String TAG = SendShareDialog.class.getSimpleName(); public static final String PACKAGE_NAME = "PACKAGE_NAME"; public static final String ACTIVITY_NAME = "ACTIVITY_NAME"; private View view; private OCFile file; + private boolean hideNcSharingOptions; private FileOperationsHelper fileOperationsHelper; - public static SendShareDialog newInstance(OCFile file) { + public static SendShareDialog newInstance(OCFile file, boolean hideNcSharingOptions) { SendShareDialog dialogFragment = new SendShareDialog(); Bundle args = new Bundle(); args.putParcelable(KEY_OCFILE, file); + args.putBoolean(KEY_HIDE_NCSHARING_OPTIONS, hideNcSharingOptions); dialogFragment.setArguments(args); return dialogFragment; @@ -81,6 +88,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { view = null; file = getArguments().getParcelable(KEY_OCFILE); + hideNcSharingOptions = getArguments().getBoolean(KEY_HIDE_NCSHARING_OPTIONS, false); } @Nullable @@ -89,6 +97,9 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, view = inflater.inflate(R.layout.send_share_fragment, container, false); + LinearLayout sendShareButtons = view.findViewById(R.id.send_share_buttons); + View divider = view.findViewById(R.id.divider); + // Share with people TextView sharePeopleText = view.findViewById(R.id.share_people_button); sharePeopleText.setOnClickListener(v -> shareFile(file)); @@ -105,7 +116,10 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, themeShareButtonImage(shareLinkImageView); shareLinkImageView.setOnClickListener(v -> shareFile(file)); - if (file.isSharedWithMe() && !file.canReshare()) { + if (hideNcSharingOptions) { + sendShareButtons.setVisibility(View.GONE); + divider.setVisibility(View.GONE); + } else if (file.isSharedWithMe() && !file.canReshare()) { showResharingNotAllowedSnackbar(); if (file.isFolder()) { @@ -216,7 +230,11 @@ private Intent createSendIntent() { } private void shareFile(OCFile file) { - fileOperationsHelper.showShareFile(file); + if (getActivity() instanceof FileDisplayActivity) { + ((FileDisplayActivity) getActivity()).showDetails(file, 1); + } else { + fileOperationsHelper.showShareFile(file); + } dismiss(); } diff --git a/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java b/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java index 744649f260cb..55aa75fb3a79 100644 --- a/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java +++ b/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java @@ -1,7 +1,10 @@ -/** +/* * ownCloud Android client application + * * @author masensio - * Copyright (C) 2015 ownCloud Inc. + * @author Andy Scherzinger + * Copyright (C) 2015 ownCloud GmbH. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -14,7 +17,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ package com.owncloud.android.ui.dialog; @@ -23,11 +25,13 @@ import android.graphics.PorterDuff; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.widget.EditText; import android.widget.TextView; @@ -42,16 +46,14 @@ * * Triggers the share when the password is introduced. */ - -public class SharePasswordDialogFragment extends DialogFragment - implements DialogInterface.OnClickListener { +public class SharePasswordDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { private static final String ARG_FILE = "FILE"; private static final String ARG_CREATE_SHARE = "CREATE_SHARE"; public static final String PASSWORD_FRAGMENT = "PASSWORD_FRAGMENT"; - private OCFile mFile; - private boolean mCreateShare; + private OCFile file; + private boolean createShare; @Override public void onStart() { @@ -60,16 +62,18 @@ public void onStart() { AlertDialog alertDialog = (AlertDialog) getDialog(); alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ThemeUtils.primaryAccentColor(getContext())); alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(ThemeUtils.primaryAccentColor(getContext())); + alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) + .setTextColor(getResources().getColor(R.color.highlight_textColor_Warning)); } /** * Public factory method to create new SharePasswordDialogFragment instances. * - * @param file OCFile bound to the public share that which password will be set or updated - * @param createShare When 'true', the request for password will be followed by the creation of a new - * public link; when 'false', a public share is assumed to exist, and the password - * is bound to it. - * @return Dialog ready to show. + * @param file OCFile bound to the public share that which password will be set or updated + * @param createShare When 'true', the request for password will be followed by the creation of a new + * public link; when 'false', a public share is assumed to exist, and the password + * is bound to it. + * @return Dialog ready to show. */ public static SharePasswordDialogFragment newInstance(OCFile file, boolean createShare) { SharePasswordDialogFragment frag = new SharePasswordDialogFragment(); @@ -80,11 +84,17 @@ public static SharePasswordDialogFragment newInstance(OCFile file, boolean creat return frag; } + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return super.onCreateView(inflater, container, savedInstanceState); + } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - mFile = getArguments().getParcelable(ARG_FILE); - mCreateShare = getArguments().getBoolean(ARG_CREATE_SHARE, false); + file = getArguments().getParcelable(ARG_FILE); + createShare = getArguments().getBoolean(ARG_CREATE_SHARE, false); // Inflate the layout for the dialog LayoutInflater inflater = getActivity().getLayoutInflater(); @@ -92,7 +102,10 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { // Setup layout EditText inputText = v.findViewById(R.id.share_password); - inputText.getBackground().setColorFilter(ThemeUtils.primaryAccentColor(getContext()), PorterDuff.Mode.SRC_ATOP); + inputText.getBackground().setColorFilter( + ThemeUtils.primaryAccentColor(getContext()), + PorterDuff.Mode.SRC_ATOP + ); inputText.setText(""); inputText.requestFocus(); @@ -102,19 +115,17 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { builder.setView(v) .setPositiveButton(R.string.common_ok, this) .setNegativeButton(R.string.common_cancel, this) + .setNeutralButton(R.string.common_delete, this) .setTitle(R.string.share_link_password_title); Dialog d = builder.create(); d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); return d; } - @Override public void onClick(DialogInterface dialog, int which) { if (which == AlertDialog.BUTTON_POSITIVE) { - String password = - ((TextView)(getDialog().findViewById(R.id.share_password))) - .getText().toString(); + String password = ((TextView) (getDialog().findViewById(R.id.share_password))).getText().toString(); if (password.length() <= 0) { Snackbar.make( @@ -125,16 +136,17 @@ public void onClick(DialogInterface dialog, int which) { return; } - if (mCreateShare) { - // Share the file - ((FileActivity) getActivity()).getFileOperationsHelper(). - shareFileViaLink(mFile, password); + setPassword(createShare, file, password); + } else if (which == AlertDialog.BUTTON_NEUTRAL) { + setPassword(createShare, file, null); + } + } - } else { - // updat existing link - ((FileActivity) getActivity()).getFileOperationsHelper(). - setPasswordToShareViaLink(mFile, password); - } + private void setPassword(boolean createShare, OCFile file, String password) { + if (createShare) { + ((FileActivity) getActivity()).getFileOperationsHelper().shareFileViaLink(file, password); + } else { + ((FileActivity) getActivity()).getFileOperationsHelper().setPasswordToShareViaLink(file, password); } } } diff --git a/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java b/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java index bc8c3a93bb46..f20d8a01246d 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java +++ b/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java @@ -28,6 +28,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -143,31 +144,25 @@ public View onCreateView(@NonNull LayoutInflater inflater, fetchAndSetData(null); - swipeListRefreshLayout.setOnRefreshListener(() -> { - setLoadingMessage(); - if (swipeListRefreshLayout != null && swipeListRefreshLayout.isRefreshing()) { - swipeListRefreshLayout.setRefreshing(false); - } - fetchAndSetData(null); - } - ); - - swipeEmptyListRefreshLayout.setOnRefreshListener(() -> { - setLoadingMessage(); - if (swipeEmptyListRefreshLayout != null && swipeEmptyListRefreshLayout.isRefreshing()) { - swipeEmptyListRefreshLayout.setRefreshing(false); - } - fetchAndSetData(null); - } - ); + swipeListRefreshLayout.setOnRefreshListener( + () -> onRefreshListLayout(swipeListRefreshLayout)); + swipeEmptyListRefreshLayout.setOnRefreshListener( + () -> onRefreshListLayout(swipeEmptyListRefreshLayout)); return view; } + private void onRefreshListLayout(SwipeRefreshLayout refreshLayout) { + setLoadingMessage(); + if (refreshLayout != null && refreshLayout.isRefreshing()) { + refreshLayout.setRefreshing(false); + } + fetchAndSetData(null); + } + private void setLoadingMessage() { emptyContentHeadline.setText(R.string.file_list_loading); emptyContentMessage.setText(""); - emptyContentIcon.setVisibility(View.GONE); emptyContentProgressBar.setVisibility(View.VISIBLE); } @@ -216,6 +211,11 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) { private void fetchAndSetData(String pageUrl) { final Account currentAccount = AccountUtils.getCurrentOwnCloudAccount(MainApp.getAppContext()); final Context context = MainApp.getAppContext(); + final FragmentActivity activity = getActivity(); + + final SwipeRefreshLayout empty = swipeEmptyListRefreshLayout; + final SwipeRefreshLayout list = swipeListRefreshLayout; + Thread t = new Thread(() -> { OwnCloudAccount ocAccount; @@ -241,15 +241,15 @@ private void fetchAndSetData(String pageUrl) { final ArrayList activities = (ArrayList) data.get(0); nextPageUrl = (String) data.get(1); - getActivity().runOnUiThread(() -> { + activity.runOnUiThread(() -> { populateList(activities, ownCloudClient, pageUrl == null); if (activities.size() > 0) { - swipeEmptyListRefreshLayout.setVisibility(View.GONE); - swipeListRefreshLayout.setVisibility(View.VISIBLE); + empty.setVisibility(View.GONE); + list.setVisibility(View.VISIBLE); } else { setEmptyContent(noResultsHeadline, noResultsMessage); - swipeListRefreshLayout.setVisibility(View.GONE); - swipeEmptyListRefreshLayout.setVisibility(View.VISIBLE); + list.setVisibility(View.GONE); + empty.setVisibility(View.VISIBLE); } isLoadingActivities = false; }); @@ -261,14 +261,13 @@ private void fetchAndSetData(String pageUrl) { logMessage = noResultsMessage; } final String finalLogMessage = logMessage; - getActivity().runOnUiThread(() -> { - setEmptyContent(noResultsHeadline, finalLogMessage); + activity.runOnUiThread(() -> { + setErrorContent(finalLogMessage); isLoadingActivities = false; - //setIndeterminate(isLoadingActivities); }); } - hideRefreshLayoutLoader(); + hideRefreshLayoutLoader(activity); } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) { Log_OC.e(TAG, "Account not found", e); } catch (IOException e) { @@ -290,16 +289,30 @@ private void populateList(List activities, OwnCloudClient mClient, boole private void setEmptyContent(String headline, String message) { if (emptyContentContainer != null && emptyContentMessage != null) { + emptyContentIcon.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_activity_light_grey)); emptyContentHeadline.setText(headline); emptyContentMessage.setText(message); + emptyContentMessage.setVisibility(View.VISIBLE); + emptyContentProgressBar.setVisibility(View.GONE); + emptyContentIcon.setVisibility(View.VISIBLE); + } + } + + private void setErrorContent(String message) { + if (emptyContentContainer != null && emptyContentMessage != null) { + emptyContentHeadline.setText(R.string.common_error); + emptyContentIcon.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_alert_octagon)); + emptyContentMessage.setText(message); + + emptyContentMessage.setVisibility(View.VISIBLE); emptyContentProgressBar.setVisibility(View.GONE); emptyContentIcon.setVisibility(View.VISIBLE); } } - private void hideRefreshLayoutLoader() { - getActivity().runOnUiThread(() -> { + private void hideRefreshLayoutLoader(FragmentActivity activity) { + activity.runOnUiThread(() -> { if (swipeListRefreshLayout != null) { swipeListRefreshLayout.setRefreshing(false); } @@ -307,7 +320,6 @@ private void hideRefreshLayoutLoader() { swipeEmptyListRefreshLayout.setRefreshing(false); } isLoadingActivities = false; - //setIndeterminate(isLoadingActivities); }); } diff --git a/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 108b9266cb41..8d285e9db6f7 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -3,8 +3,10 @@ * * @author Bartek Przybylski * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2011 Bartek Przybylski * Copyright (C) 2016 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -17,16 +19,15 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ package com.owncloud.android.ui.fragment; import android.accounts.Account; +import android.content.Context; import android.graphics.Bitmap; -import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; -import android.support.design.widget.Snackbar; +import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; import android.support.v4.view.ViewPager; import android.view.LayoutInflater; @@ -35,6 +36,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.ProgressBar; @@ -62,27 +64,64 @@ import java.lang.ref.WeakReference; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; /** * This Fragment is used to display the details about a file. */ public class FileDetailFragment extends FileFragment implements OnClickListener { + private static final String TAG = FileDetailFragment.class.getSimpleName(); + public static final String FTAG_CONFIRMATION = "REMOVE_CONFIRMATION_FRAGMENT"; + public static final String FTAG_RENAME_FILE = "RENAME_FILE_FRAGMENT"; + + private static final String ARG_FILE = "FILE"; + private static final String ARG_ACCOUNT = "ACCOUNT"; + private static final String ARG_ACTIVE_TAB = "TAB"; + + @Nullable @BindView(R.id.progressBlock) + View downloadProgressContainer; + + @Nullable @BindView(R.id.cancelBtn) + ImageButton cancelButton; + + @Nullable @BindView(R.id.progressBar) + ProgressBar progressBar; + + @Nullable @BindView(R.id.progressText) + TextView progressText; + + @Nullable @BindView(R.id.filename) + TextView fileName; + + @Nullable @BindView(R.id.size) + TextView fileSize; + + @Nullable @BindView(R.id.modified) + TextView fileModifiedTimestamp; + + @Nullable @BindView(R.id.favorite) + ImageView favoriteIcon; + + @Nullable @BindView(R.id.overflow_menu) + ImageView overflowMenu; + + @Nullable @BindView(R.id.tab_layout) + TabLayout tabLayout; + + @Nullable @BindView(R.id.pager) + ViewPager viewPager; private int layout; private View view; - private ImageView previewImage; - private ProgressBar toolbarProgressBar; private boolean previewLoaded; private Account account; + private Unbinder unbinder; public ProgressListener progressListener; - - private static final String TAG = FileDetailFragment.class.getSimpleName(); - public static final String FTAG_CONFIRMATION = "REMOVE_CONFIRMATION_FRAGMENT"; - public static final String FTAG_RENAME_FILE = "RENAME_FILE_FRAGMENT"; - - private static final String ARG_FILE = "FILE"; - private static final String ARG_ACCOUNT = "ACCOUNT"; + private ToolbarActivity activity; + private int activeTab; /** * Public factory method to create new FileDetailFragment instances. @@ -102,6 +141,26 @@ public static FileDetailFragment newInstance(OCFile fileToDetail, Account accoun return frag; } + /** + * Public factory method to create new FileDetailFragment instances. + * + * When 'fileToDetail' or 'ocAccount' are null, creates a dummy layout (to use when a file wasn't tapped before). + * + * @param fileToDetail An {@link OCFile} to show in the fragment + * @param account An ownCloud account; needed to start downloads + * @param activeTab to be active tab + * @return New fragment with arguments set + */ + public static FileDetailFragment newInstance(OCFile fileToDetail, Account account, int activeTab) { + FileDetailFragment frag = new FileDetailFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_FILE, fileToDetail); + args.putParcelable(ARG_ACCOUNT, account); + args.putInt(ARG_ACTIVE_TAB, activeTab); + frag.setArguments(args); + return frag; + } + /** * Creates an empty details fragment. * @@ -111,25 +170,38 @@ public static FileDetailFragment newInstance(OCFile fileToDetail, Account accoun public FileDetailFragment() { super(); account = null; - layout = R.layout.file_details_empty; + layout = R.layout.empty_list; progressListener = null; } + /** + * return the reference to the file detail sharing fragment to communicate with it. + * + * @return reference to the {@link FileDetailSharingFragment} + */ + public FileDetailSharingFragment getFileDetailSharingFragment() { + return ((FileDetailTabAdapter)viewPager.getAdapter()).getFileDetailSharingFragment(); + } + @Override public void onResume() { super.onResume(); - if (previewImage != null && MimeTypeUtil.isImage(getFile()) && previewLoaded) { + if (getFile()!= null && MimeTypeUtil.isImage(getFile()) && previewLoaded) { activatePreviewImage(); } } private void activatePreviewImage() { - previewImage.setVisibility(View.VISIBLE); - toolbarProgressBar.setVisibility(View.GONE); - ((ToolbarActivity) getActivity()).getSupportActionBar().setTitle(null); - ((ToolbarActivity) getActivity()).getSupportActionBar().setBackgroundDrawable(null); - makeStatusBarBlack(); + if (activity != null) { + activity.setPreviewImageVisibility(View.VISIBLE); + activity.setProgressBarVisibility(View.GONE); + ThemeUtils.setStatusBarColor(activity, activity.getResources().getColor(R.color.black)); + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(null); + activity.getSupportActionBar().setBackgroundDrawable(null); + } + } } @Override @@ -139,105 +211,50 @@ public void onActivityCreated(Bundle savedInstanceState) { } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { setFile(getArguments().getParcelable(ARG_FILE)); account = getArguments().getParcelable(ARG_ACCOUNT); + activeTab = getArguments().getInt(ARG_ACTIVE_TAB, 0); if (savedInstanceState != null) { setFile(savedInstanceState.getParcelable(FileActivity.EXTRA_FILE)); account = savedInstanceState.getParcelable(FileActivity.EXTRA_ACCOUNT); } - toolbarProgressBar = getActivity().findViewById(R.id.progressBar); - if (getFile() != null && account != null) { layout = R.layout.file_details_fragment; } view = inflater.inflate(layout, null); - - if (layout == R.layout.file_details_fragment) { - ProgressBar progressBar = view.findViewById(R.id.fdProgressBar); - ThemeUtils.colorHorizontalProgressBar(progressBar, ThemeUtils.primaryAccentColor(getContext())); - progressListener = new ProgressListener(progressBar); - view.findViewById(R.id.fdCancelBtn).setOnClickListener(this); - view.findViewById(R.id.fdFavorite).setOnClickListener(this); - view.findViewById(R.id.overflow_menu).setOnClickListener(this); - previewImage = getActivity().findViewById(R.id.preview_image); - // TODO use whenever we switch to use glide for preview images - /* - if (getFile() != null && account != null && MimeTypeUtil.isImage(getFile())) { - setHeaderImage(); - } - */ - } + unbinder = ButterKnife.bind(this, view); - updateFileDetails(false, false); return view; } - // TODO use whenever we switch to use glide for preview images - /* - private void setHeaderImage() { - if (mContainerActivity.getStorageManager().getCapability(account.name) - .getServerBackground() != null && previewImage != null) { - - String background = mContainerActivity.getStorageManager().getCapability(account.name).getServerBackground(); - - int primaryColor = ThemeUtils.primaryColor(account, getContext()); - - if (URLUtil.isValidUrl(background)) { - // background image - SimpleTarget target = new SimpleTarget() { - @Override - public void onResourceReady(Drawable resource, GlideAnimation glideAnimation) { - Drawable[] drawables = {new ColorDrawable(primaryColor), resource}; - LayerDrawable layerDrawable = new LayerDrawable(drawables); - previewImage.setImageDrawable(layerDrawable); - previewImage.setVisibility(View.VISIBLE); - ((ToolbarActivity) getActivity()).getSupportActionBar().setTitle(null); - ((ToolbarActivity) getActivity()).getSupportActionBar().setBackgroundDrawable(null); - toolbarProgressBar.setVisibility(View.GONE); - previewLoaded = true; - } - @Override - public void onLoadFailed(Exception e, Drawable errorDrawable) { - previewImage.setVisibility(View.GONE); - toolbarProgressBar.setVisibility(View.VISIBLE); - } - }; - - Glide.with(this) - .load(background) - .centerCrop() - .placeholder(R.drawable.background) - .error(R.drawable.background) - .crossFade() - .into(target); - } else { - // hide image - previewImage.setVisibility(View.GONE); - toolbarProgressBar.setVisibility(View.VISIBLE); - } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + if (getFile() != null && account != null) { + ThemeUtils.colorHorizontalProgressBar(progressBar, ThemeUtils.primaryAccentColor(getContext())); + progressListener = new ProgressListener(progressBar); + cancelButton.setOnClickListener(this); + favoriteIcon.setOnClickListener(this); + overflowMenu.setOnClickListener(this); + + updateFileDetails(false, false); } } - */ public void onOverflowIconClicked(View view) { PopupMenu popup = new PopupMenu(getActivity(), view); - popup.inflate(R.menu.file_actions_menu); + popup.inflate(R.menu.file_details_actions_menu); prepareOptionsMenu(popup.getMenu()); - popup.setOnMenuItemClickListener(item -> { - return optionsItemSelected(item); - }); + popup.setOnMenuItemClickListener(this::optionsItemSelected); popup.show(); } - private void setupViewPager(View view) { - TabLayout tabLayout = view.findViewById(R.id.tab_layout); + private void setupViewPager() { tabLayout.removeAllTabs(); tabLayout.addTab(tabLayout.newTab().setText(R.string.drawer_item_activities)); @@ -246,7 +263,6 @@ private void setupViewPager(View view) { tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); tabLayout.setSelectedTabIndicatorColor(ThemeUtils.primaryAccentColor(getContext())); - final ViewPager viewPager = view.findViewById(R.id.pager); final FileDetailTabAdapter adapter = new FileDetailTabAdapter (getFragmentManager(), getFile(), account); viewPager.setAdapter(adapter); @@ -267,6 +283,8 @@ public void onTabReselected(TabLayout.Tab tab) { // unused at the moment } }); + + tabLayout.getTabAt(activeTab).select(); } @Override @@ -285,17 +303,32 @@ public void onStart() { @Override public void onStop() { leaveTransferProgress(); - if (previewImage != null) { - previewImage.setVisibility(View.GONE); - toolbarProgressBar.setVisibility(View.VISIBLE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - getActivity().getWindow().setStatusBarColor(ThemeUtils.primaryDarkColor(getContext())); - } + + if(activity != null) { + activity.setPreviewImageVisibility(View.GONE); + activity.setProgressBarVisibility(View.VISIBLE); + activity.setupToolbar(); } super.onStop(); } + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof ToolbarActivity) { + activity = (ToolbarActivity) context; + } else { + activity = null; + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + unbinder.unbind(); + } + @Override public View getView() { return super.getView() == null ? view : super.getView(); @@ -320,44 +353,23 @@ private void prepareOptionsMenu(Menu menu) { mf.filter(menu, true); } - // restriction for this fragment - FileMenuFilter.hideMenuItems( - menu.findItem(R.id.action_see_details), - menu.findItem(R.id.action_select_all), - menu.findItem(R.id.action_move), - menu.findItem(R.id.action_copy), - menu.findItem(R.id.action_favorite), - menu.findItem(R.id.action_unset_favorite), - menu.findItem(R.id.action_search) - ); + if (getFile().isFolder()) { + FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_send_file)); + FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_sync_file)); + } // dual pane restrictions if (!getResources().getBoolean(R.bool.large_land_layout)){ - FileMenuFilter.hideMenuItems( - menu.findItem(R.id.action_switch_view), - menu.findItem(R.id.action_sync_account), - menu.findItem(R.id.action_sort) - ); + FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_sync_account)); } - // share restrictions - if (getFile().isSharedWithMe() && !getFile().canReshare()) { - FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_send_share_file)); - } + } - public boolean optionsItemSelected(MenuItem item) { + private boolean optionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.action_send_share_file: { - if(getFile().isSharedWithMe() && !getFile().canReshare()){ - Snackbar.make(getView(), - R.string.resharing_is_not_allowed, - Snackbar.LENGTH_LONG - ) - .show(); - } else { - mContainerActivity.getFileOperationsHelper().sendShareFile(getFile()); - } + case R.id.action_send_file: { + mContainerActivity.getFileOperationsHelper().sendShareFile(getFile(), true); return true; } case R.id.action_open_file_with: { @@ -391,6 +403,14 @@ public boolean optionsItemSelected(MenuItem item) { mContainerActivity.getFileOperationsHelper().toggleOfflineFile(getFile(), false); return true; } + case R.id.action_encrypted: { + // TODO implement or remove + return true; + } + case R.id.action_unset_encrypted: { + // TODO implement or remove + return true; + } default: return super.onOptionsItemSelected(item); } @@ -399,22 +419,17 @@ public boolean optionsItemSelected(MenuItem item) { @Override public void onClick(View v) { switch (v.getId()) { - case R.id.fdCancelBtn: { + case R.id.cancelBtn: { ((FileDisplayActivity) mContainerActivity).cancelTransference(getFile()); break; } - case R.id.fdFavorite: { + case R.id.favorite: { if (getFile().getIsFavorite()) { - ((ImageView)getView().findViewById(R.id.fdFavorite)). - setImageDrawable(getResources() - .getDrawable(R.drawable.ic_star_outline)); mContainerActivity.getFileOperationsHelper().toggleFavoriteFile(getFile(), false); } else { - ((ImageView)getView().findViewById(R.id.fdFavorite)) - .setImageDrawable(getResources() - .getDrawable(R.drawable.ic_star)); mContainerActivity.getFileOperationsHelper().toggleFavoriteFile(getFile(), true); } + setFavoriteIconStatus(!getFile().getIsFavorite()); break; } case R.id.overflow_menu: { @@ -433,7 +448,7 @@ public void onClick(View v) { * @return True when the fragment was created with the empty layout. */ public boolean isEmpty() { - return (layout == R.layout.file_details_empty || getFile() == null || account == null); + return (layout == R.layout.empty_list || getFile() == null || account == null); } /** @@ -471,16 +486,15 @@ public void updateFileDetails(boolean transferring, boolean refresh) { OCFile file = getFile(); // set file details - setFilename(file.getFileName()); - setFiletype(file); - setFilesize(file.getFileLength()); - setTimeModified(file.getModificationTimestamp()); - - if (file.getIsFavorite()) { - ((ImageView)getView().findViewById(R.id.fdFavorite)).setImageDrawable(getResources().getDrawable(R.drawable.ic_star)); + if (MimeTypeUtil.isImage(file)) { + fileName.setText(file.getFileName()); } else { - ((ImageView)getView().findViewById(R.id.fdFavorite)).setImageDrawable(getResources().getDrawable(R.drawable.ic_star_outline)); + fileName.setVisibility(View.GONE); } + fileSize.setText(DisplayUtils.bytesToHumanReadable(file.getFileLength())); + fileModifiedTimestamp.setText(DisplayUtils.getRelativeTimestamp(getContext(), file.getModificationTimestamp())); + setFilePreview(file); + setFavoriteIconStatus(file.getIsFavorite()); // configure UI for depending upon local state of the file FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder(); @@ -502,11 +516,19 @@ public void updateFileDetails(boolean transferring, boolean refresh) { } } - setupViewPager(getView()); + setupViewPager(); getView().invalidate(); } + private void setFavoriteIconStatus(boolean isFavorite) { + if (isFavorite) { + favoriteIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_star)); + } else { + favoriteIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_star_outline)); + } + } + /** * Checks if the fragment is ready to show details of a OCFile * @@ -517,124 +539,68 @@ private boolean readyToShow() { } /** - * Updates the filename in view + * Updates the file preview if possible * - * @param filename to set + * @param file a {@link OCFile} to be previewed */ - private void setFilename(String filename) { - TextView tv = getView().findViewById(R.id.fdFilename); - if (tv != null) { - tv.setText(filename); - } - } + private void setFilePreview(OCFile file) { + Bitmap resizedImage; - /** - * Updates the MIME type in view - * @param file : An {@link OCFile} - */ - private void setFiletype(OCFile file) { - ImageView iv = getView().findViewById(R.id.fdIcon); - View v = getView().findViewById(R.id.fdIcon_divider); + if (MimeTypeUtil.isImage(file) && activity != null) { + String tagId = String.valueOf(ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + getFile().getRemoteId()); + resizedImage = ThumbnailsCacheManager.getBitmapFromDiskCache(tagId); - if (iv != null) { - iv.setTag(file.getFileId()); - // Name of the file, to deduce the icon to use in case the MIME type is not precise enough - iv.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimetype(), file.getFileName(), account, - getContext())); + if (resizedImage != null && !file.needsUpdateThumbnail()) { + activity.setPreviewImageBitmap(resizedImage); + activatePreviewImage(); + previewLoaded = true; + } else { + // show thumbnail while loading resized image + Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + String.valueOf(ThumbnailsCacheManager.PREFIX_THUMBNAIL + getFile().getRemoteId())); - Bitmap resizedImage; + if (thumbnail != null) { + activity.setPreviewImageBitmap(thumbnail); + } else { + thumbnail = ThumbnailsCacheManager.mDefaultImg; + } - if (MimeTypeUtil.isImage(file)) { - String tagId = String.valueOf(ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + getFile().getRemoteId()); - resizedImage = ThumbnailsCacheManager.getBitmapFromDiskCache(tagId); + // generate new resized image + if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(getFile(), activity.getPreviewImageView()) && + mContainerActivity.getStorageManager() != null) { + final ThumbnailsCacheManager.ResizedImageGenerationTask task = + new ThumbnailsCacheManager.ResizedImageGenerationTask(FileDetailFragment.this, + activity.getPreviewImageView(), + mContainerActivity.getStorageManager(), + mContainerActivity.getStorageManager().getAccount()); + + if (resizedImage == null) { + resizedImage = thumbnail; + } + + final ThumbnailsCacheManager.AsyncResizedImageDrawable asyncDrawable = + new ThumbnailsCacheManager.AsyncResizedImageDrawable( + MainApp.getAppContext().getResources(), + resizedImage, + task + ); - if (resizedImage != null && !file.needsUpdateThumbnail()) { - iv.setVisibility(View.GONE); - v.setVisibility(View.GONE); - previewImage.setImageBitmap(resizedImage); + activity.setPreviewImageDrawable(asyncDrawable); activatePreviewImage(); previewLoaded = true; - } else { - // show thumbnail while loading resized image - Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - String.valueOf(ThumbnailsCacheManager.PREFIX_THUMBNAIL + getFile().getRemoteId())); - - if (thumbnail != null) { - iv.setImageBitmap(thumbnail); - } else { - thumbnail = ThumbnailsCacheManager.mDefaultImg; - } - - // generate new resized image - if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(getFile(), iv) && - mContainerActivity.getStorageManager() != null) { - final ThumbnailsCacheManager.ResizedImageGenerationTask task = - new ThumbnailsCacheManager.ResizedImageGenerationTask(FileDetailFragment.this, - iv, - mContainerActivity.getStorageManager(), - mContainerActivity.getStorageManager().getAccount()); - if (resizedImage == null) { - resizedImage = thumbnail; - } - final ThumbnailsCacheManager.AsyncResizedImageDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncResizedImageDrawable( - MainApp.getAppContext().getResources(), - resizedImage, - task - ); - iv.setVisibility(View.GONE); - v.setVisibility(View.GONE); - previewImage.setImageDrawable(asyncDrawable); - activatePreviewImage(); - previewLoaded = true; - task.execute(getFile()); - } + task.execute(getFile()); } - } else { - iv.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimetype(), file.getFileName(), account, - getContext())); } } } - private void makeStatusBarBlack() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.black)); - } - } - - /** - * Updates the file size in view - * - * @param fileSize in bytes to set - */ - private void setFilesize(long fileSize) { - TextView tv = getView().findViewById(R.id.fdSize); - if (tv != null) { - tv.setText(DisplayUtils.bytesToHumanReadable(fileSize)); - } - } - - /** - * Updates the time that the file was last modified - * - * @param milliseconds Unix time to set - */ - private void setTimeModified(long milliseconds) { - TextView tv = getView().findViewById(R.id.fdModified); - if (tv != null) { - tv.setText(DisplayUtils.unixTimeToHumanReadable(milliseconds)); - } - } - /** * Enables or disables buttons for a file being downloaded */ private void setButtonsForTransferring() { if (!isEmpty()) { // show the progress bar for the transfer - getView().findViewById(R.id.fdProgressBlock).setVisibility(View.VISIBLE); - TextView progressText = getView().findViewById(R.id.fdProgressText); + downloadProgressContainer.setVisibility(View.VISIBLE); progressText.setVisibility(View.VISIBLE); FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder(); FileUploaderBinder uploaderBinder = mContainerActivity.getFileUploaderBinder(); @@ -656,9 +622,7 @@ private void setButtonsForTransferring() { private void setButtonsForDown() { if (!isEmpty()) { // hides the progress bar - getView().findViewById(R.id.fdProgressBlock).setVisibility(View.GONE); - TextView progressText = getView().findViewById(R.id.fdProgressText); - progressText.setVisibility(View.GONE); + downloadProgressContainer.setVisibility(View.GONE); } } @@ -668,9 +632,7 @@ private void setButtonsForDown() { private void setButtonsForRemote() { if (!isEmpty()) { // hides the progress bar - getView().findViewById(R.id.fdProgressBlock).setVisibility(View.GONE); - TextView progressText = getView().findViewById(R.id.fdProgressText); - progressText.setVisibility(View.GONE); + downloadProgressContainer.setVisibility(View.GONE); } } @@ -706,25 +668,25 @@ public void leaveTransferProgress() { * Helper class responsible for updating the progress bar shown for file downloading. */ private class ProgressListener implements OnDatatransferProgressListener { - private int mLastPercent = 0; - private WeakReference mProgressBar; + private int lastPercent = 0; + private WeakReference progressBarReference; ProgressListener(ProgressBar progressBar) { - mProgressBar = new WeakReference<>(progressBar); + progressBarReference = new WeakReference<>(progressBar); } @Override public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String filename) { int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer)); - if (percent != mLastPercent) { - ProgressBar pb = mProgressBar.get(); + if (percent != lastPercent) { + ProgressBar pb = progressBarReference.get(); if (pb != null) { pb.setProgress(percent); pb.postInvalidate(); } } - mLastPercent = percent; + lastPercent = percent; } } } diff --git a/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index ccecf47cbb27..215a35a6f639 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -21,37 +21,87 @@ package com.owncloud.android.ui.fragment; import android.accounts.Account; +import android.app.SearchManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; +import android.support.v7.widget.AppCompatCheckBox; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.ListView; +import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.TextView; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.resources.shares.OCShare; +import com.owncloud.android.lib.resources.shares.SharePermissionsBuilder; +import com.owncloud.android.lib.resources.shares.ShareType; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.adapter.UserListAdapter; +import com.owncloud.android.ui.decoration.SimpleListItemDividerDecoration; +import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment; +import com.owncloud.android.ui.dialog.SharePasswordDialogFragment; +import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper; import com.owncloud.android.utils.ThemeUtils; import java.util.ArrayList; -public class FileDetailSharingFragment extends Fragment { +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; + +public class FileDetailSharingFragment extends Fragment implements UserListAdapter.ShareeListAdapterListener { private static final String ARG_FILE = "FILE"; private static final String ARG_ACCOUNT = "ACCOUNT"; // to show share with users/groups info - private ArrayList mShares; + private ArrayList shares; private OCFile file; private Account account; + private OCCapability capabilities; + private OCShare publicShare; + + private Unbinder unbinder; + + @BindView(R.id.searchView) + SearchView searchView; + + @BindView(R.id.shareUsersList) + RecyclerView usersList; + + @BindView(R.id.shareNoUsers) + TextView noList; + + @BindView(R.id.share_by_link) + AppCompatCheckBox shareByLink; + + @BindView(R.id.overflow_menu_share_link) + ImageView overflowMenuShareLink; + + @BindView(R.id.share_by_link_allow_editing) + AppCompatCheckBox shareByLinkAllowEditing; + + @BindView(R.id.share_by_link_container) + LinearLayout shareByLinkContainer; public static FileDetailSharingFragment newInstance(OCFile file, Account account) { FileDetailSharingFragment fragment = new FileDetailSharingFragment(); @@ -63,9 +113,15 @@ public static FileDetailSharingFragment newInstance(OCFile file, Account account } @Override - public View onCreateView(@NonNull LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState) { + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + refreshCapabilitiesFromDB(); + refreshPublicShareFromDB(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { file = getArguments().getParcelable(ARG_FILE); account = getArguments().getParcelable(ARG_ACCOUNT); @@ -76,102 +132,331 @@ public View onCreateView(@NonNull LayoutInflater inflater, } View view = inflater.inflate(R.layout.file_details_sharing_fragment, container, false); + unbinder = ButterKnife.bind(this, view); - setupView(view); + setupView(); return view; } - private void setupView(View view) { - ((TextView)view.findViewById(R.id.fdShareTitle)).setTextColor(ThemeUtils.primaryAccentColor(getContext())); - ((TextView)view.findViewById(R.id.fdShareWithUsersTitle)).setTextColor(ThemeUtils.primaryAccentColor(getContext())); + @Override + public void onDestroy() { + super.onDestroy(); + unbinder.unbind(); + } - setShareByLinkInfo(file.isSharedViaLink(), view); + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (!(getActivity() instanceof FileActivity)) { + throw new IllegalArgumentException("Calling activity must be of type FileActivity"); + } + } - setShareWithUserInfo(view); + private void setupView() { + setShareByLinkInfo(file.isSharedViaLink()); + setShareWithUserInfo(); + FileDetailSharingFragmentHelper.setupSearchView( + (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE), + searchView, + getActivity().getComponentName() + ); } /** - * Updates Share by link data + * Updates Share by link UI * * @param isShareByLink flag is share by link is enable */ - private void setShareByLinkInfo(boolean isShareByLink, View view) { - TextView tv = view.findViewById(R.id.fdSharebyLink); - if (tv != null) { - tv.setText(isShareByLink ? R.string.filedetails_share_link_enable : - R.string.filedetails_share_link_disable); - } - ImageView linkIcon = view.findViewById(R.id.fdShareLinkIcon); - if (linkIcon != null) { - linkIcon.setVisibility(isShareByLink ? View.VISIBLE : View.GONE); + public void setShareByLinkInfo(boolean isShareByLink) { + shareByLink.setChecked(isShareByLink); + int accentColor = ThemeUtils.primaryAccentColor(getContext()); + ThemeUtils.tintCheckbox(shareByLink, accentColor); + ThemeUtils.tintCheckbox(shareByLinkAllowEditing, accentColor); + setLinkDetailVisible(isShareByLink); + } + + private void setLinkDetailVisible(boolean visible) { + if (visible) { + if (file.isFolder()) { + shareByLinkAllowEditing.setVisibility(View.VISIBLE); + } else { + shareByLinkAllowEditing.setVisibility(View.INVISIBLE); + } + overflowMenuShareLink.setVisibility(View.VISIBLE); + } else { + shareByLinkAllowEditing.setVisibility(View.INVISIBLE); + overflowMenuShareLink.setVisibility(View.INVISIBLE); } } /** * Update Share With data */ - private void setShareWithUserInfo(View view){ + public void setShareWithUserInfo() { // Get Users and Groups if (((FileActivity) getActivity()).getStorageManager() != null) { FileDataStorageManager fileDataStorageManager = ((FileActivity) getActivity()).getStorageManager(); - mShares = fileDataStorageManager.getSharesWithForAFile( - file.getRemotePath(),account.name - ); + + shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), account.name); // Update list of users/groups - updateListOfUserGroups(view); + updateListOfUserGroups(); } } - private void updateListOfUserGroups(View view) { - // Update list of users/groups + private void updateListOfUserGroups() { // TODO Refactoring: create a new {@link ShareUserListAdapter} instance with every call should not be needed - UserListAdapter mUserGroupsAdapter = new UserListAdapter( - getActivity().getApplicationContext(), - R.layout.share_user_item, mShares - ); - // Show data - ListView usersList = view.findViewById(R.id.fdshareUsersList); - - // No data - TextView noList = view.findViewById(R.id.fdShareNoUsers); - - if (mShares.size() > 0) { + if (shares.size() > 0) { usersList.setVisibility(View.VISIBLE); - usersList.setAdapter(mUserGroupsAdapter); + usersList.setAdapter(new UserListAdapter(getActivity().getSupportFragmentManager(), + getActivity().getApplicationContext(), shares, account, file, this)); + usersList.setLayoutManager(new LinearLayoutManager(getContext())); + usersList.addItemDecoration(new SimpleListItemDividerDecoration(getContext())); noList.setVisibility(View.GONE); - setListViewHeightBasedOnChildren(usersList); - } else { usersList.setVisibility(View.GONE); noList.setVisibility(View.VISIBLE); } } + @OnClick(R.id.share_by_link) + public void toggleShareByLink() { + if (shareByLink.isChecked()) { + if (capabilities != null && + capabilities.getFilesSharingPublicPasswordEnforced().isTrue()) { + // password enforced by server, request to the user before trying to create + requestPasswordForShareViaLink(true); + + } else { + // create without password if not enforced by server or we don't know if enforced; + ((FileActivity) getActivity()).getFileOperationsHelper().shareFileViaLink(file, null); + } + + } else { + ((FileActivity) getActivity()).getFileOperationsHelper().unshareFileViaLink(file); + } + } + + @OnClick(R.id.share_link_label) + public void showSendLinkTo() { + if (file.isSharedViaLink()) { + ((FileActivity) getActivity()).getFileOperationsHelper().getFileWithLink(file); + } + } + + @OnClick(R.id.share_by_link_allow_editing) + public void toggleShareLinkAllowEditing() { + if (file.isSharedViaLink()) { + ((FileActivity) getActivity()).getFileOperationsHelper() + .setUploadPermissionsToShare(file, shareByLinkAllowEditing.isChecked()); + } + } + + @OnClick(R.id.overflow_menu_share_link) + public void showLinkOverflowMenu() { + Context context = getContext(); + if (context != null && ThemeUtils.themingEnabled(context)) { + // use grey as fallback for elements where custom theming is not available + context.getTheme().applyStyle(R.style.FallbackThemingTheme, true); + } else { + context = getActivity(); + } + + PopupMenu popup = new PopupMenu(context, overflowMenuShareLink); + popup.inflate(R.menu.file_detail_sharing_link_menu); + prepareOptionsMenu(popup.getMenu()); + popup.setOnMenuItemClickListener(this::optionsItemSelected); + popup.show(); + } + + private void prepareOptionsMenu(Menu menu) { + Resources res = getResources(); + FileDetailSharingFragmentHelper.setupHideFileListingMenuItem( + menu.findItem(R.id.action_share_link_hide_file_listing), + file.isFolder(), + shareByLinkAllowEditing.isChecked(), + publicShare.getPermissions() + ); + FileDetailSharingFragmentHelper.setupPasswordMenuItem( + menu.findItem(R.id.action_share_link_password), + publicShare.isPasswordProtected() + ); + FileDetailSharingFragmentHelper.setupExpirationDateMenuItem( + menu.findItem(R.id.action_share_link_expiration_date), + publicShare.getExpirationDate(), + res + ); + } + + private boolean optionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_share_link_hide_file_listing: { + item.setChecked(!item.isChecked()); + if (capabilities.getFilesFileDrop().isTrue()) { + ((FileActivity) getActivity()).getFileOperationsHelper(). + setHideFileListingPermissionsToShare(publicShare, item.isChecked()); + } else { + // not supported in ownCloud + Snackbar.make(getView(), R.string.files_drop_not_supported, Snackbar.LENGTH_LONG) + .setAction(R.string.learn_more, v -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(getString(R.string.url_server_install))); + startActivity(i); + }) + .show(); + } + return true; + } + case R.id.action_share_link_password: { + requestPasswordForShareViaLink(false); + return true; + } + case R.id.action_share_link_expiration_date: { + ExpirationDatePickerDialogFragment dialog = ExpirationDatePickerDialogFragment.newInstance(file, -1); + dialog.show( + getActivity().getSupportFragmentManager(), + ExpirationDatePickerDialogFragment.DATE_PICKER_DIALOG + ); + return true; + } + default: + return super.onOptionsItemSelected(item); + } + } + /** - * Fix scroll in listview when the parent is a ScrollView + * Updates the UI after the result of an update operation on the edited {@link OCFile}. + * + * @param result {@link RemoteOperationResult} of an update on the edited {@link OCFile} sharing information. + * @param file the edited {@link OCFile} */ - private static void setListViewHeightBasedOnChildren(ListView listView) { - ListAdapter listAdapter = listView.getAdapter(); - if (listAdapter == null) { - return; - } - int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.AT_MOST); - int totalHeight = 0; - View view = null; - for (int i = 0; i < listAdapter.getCount(); i++) { - view = listAdapter.getView(i, view, listView); - if (i == 0) { - view.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth, ViewGroup.LayoutParams.WRAP_CONTENT)); + public void onUpdateShareInformation(RemoteOperationResult result, OCFile file) { + this.file = file; + + if (result.isSuccess()) { + refreshUiFromDB(); + } else { + setupView(); + } + } + + /** + * Get {@link OCShare} instance from DB and updates the UI. + * + * Depends on the parent Activity provides a {@link com.owncloud.android.datamodel.FileDataStorageManager} + * instance ready to use. If not ready, does nothing. + */ + private void refreshUiFromDB() { + FileDataStorageManager storageManager = ((FileActivity) getActivity()).getStorageManager(); + if (storageManager != null) { + if (publicShare != null) { + // Get edited shared by link + publicShare = storageManager.getShareById(publicShare.getId()); } - view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED); - totalHeight += view.getMeasuredHeight(); + + // Updates UI with new state + setupView(); + } + } + + @Override + public void unshareWith(OCShare share) { + ((FileActivity) getActivity()).getFileOperationsHelper() + .unshareFileWithUserOrGroup(file, share.getShareType(), share.getShareWith()); + } + + @Override + public int updatePermissionsToShare(OCShare share, boolean canReshare, boolean canEdit, + boolean canEditCreate, boolean canEditChange, + boolean canEditDelete) { + SharePermissionsBuilder spb = new SharePermissionsBuilder(); + spb.setSharePermission(canReshare); + if (file.isFolder()) { + spb.setUpdatePermission(canEditChange) + .setCreatePermission(canEditCreate) + .setDeletePermission(canEditDelete); + } else { + spb.setUpdatePermission(canEdit); + } + int permissions = spb.build(); + + ((FileActivity) getActivity()).getFileOperationsHelper(). + setPermissionsToShare( + share, + permissions + ) + ; + + return permissions; + } + + /** + * Starts a dialog that requests a password to the user to protect a share link. + * + * @param createShare When 'true', the request for password will be followed by the creation of a new + * public link; when 'false', a public share is assumed to exist, and the password + * is bound to it. + */ + public void requestPasswordForShareViaLink(boolean createShare) { + SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(file, createShare); + dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); + } + + /** + * Get known server capabilities from DB + * + * Depends on the parent Activity provides a {@link com.owncloud.android.datamodel.FileDataStorageManager} + * instance ready to use. If not ready, does nothing. + */ + public void refreshCapabilitiesFromDB() { + if (((FileActivity) getActivity()).getStorageManager() != null) { + capabilities = ((FileActivity) getActivity()).getStorageManager().getCapability(account.name); + } + } + + /** + * Get public link from the DB to fill in the "Share link" section in the UI. + * + * Takes into account server capabilities before reading database. + * + * Depends on the parent Activity provides a {@link com.owncloud.android.datamodel.FileDataStorageManager} + * instance ready to use. If not ready, does nothing. + */ + public void refreshPublicShareFromDB() { + if (FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) { + shareByLinkContainer.setVisibility(View.GONE); + } else if (((FileActivity) getActivity()).getStorageManager() != null) { + // Get public share + publicShare = ((FileActivity) getActivity()).getStorageManager().getFirstShareByPathAndType( + file.getRemotePath(), + ShareType.PUBLIC_LINK, + "" + ); + + // Update public share section + updatePublicShareSection(); + } + } + + /** + * Updates in the UI the section about public share with the information + * in the current public share bound to, if any. + */ + private void updatePublicShareSection() { + if (publicShare != null && ShareType.PUBLIC_LINK.equals(publicShare.getShareType())) { + shareByLink.setChecked(true); + + if (publicShare.getPermissions() > OCShare.READ_PERMISSION_FLAG) { + shareByLinkAllowEditing.setChecked(true); + } else { + shareByLinkAllowEditing.setChecked(false); + } + + setShareByLinkInfo(true); + } else { + setShareByLinkInfo(false); } - ViewGroup.LayoutParams params = listView.getLayoutParams(); - params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); - listView.setLayoutParams(params); - listView.requestLayout(); } } diff --git a/src/main/java/com/owncloud/android/ui/fragment/FileFragment.java b/src/main/java/com/owncloud/android/ui/fragment/FileFragment.java index 88404a10e842..4eeb0d9b2d82 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/FileFragment.java +++ b/src/main/java/com/owncloud/android/ui/fragment/FileFragment.java @@ -1,8 +1,10 @@ -/** +/* * ownCloud Android client application * * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -15,7 +17,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ package com.owncloud.android.ui.fragment; @@ -131,6 +132,14 @@ public interface ContainerActivity extends ComponentsGetter { */ void showDetails(OCFile file); + /** + * Request the parent activity to show the details of an {@link OCFile}. + * + * @param file File to show details + * @param activeTab the active tab + */ + void showDetails(OCFile file, int activeTab); + ///// TO UNIFY IN A SINGLE CALLBACK METHOD - EVENT NOTIFICATIONs -> something happened // inside the fragment, MAYBE activity is interested --> unify in notification method diff --git a/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index c4484be437fc..ce3ce8c4a00c 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -4,8 +4,10 @@ * @author Bartek Przybylski * @author masensio * @author David A. Velasco + * @author Andy Scherzinger * Copyright (C) 2011 Bartek Przybylski * Copyright (C) 2016 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -393,7 +395,11 @@ public void uploadFiles() { @Override public void onShareIconClick(OCFile file) { - mContainerActivity.getFileOperationsHelper().sendShareFile(file); + if (file.isFolder()) { + mContainerActivity.showDetails(file, 1); + } else { + mContainerActivity.getFileOperationsHelper().sendShareFile(file); + } } @Override diff --git a/src/main/java/com/owncloud/android/ui/fragment/ShareFileFragment.java b/src/main/java/com/owncloud/android/ui/fragment/ShareFileFragment.java index 039a6db452f2..78c48e4f5f1a 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/ShareFileFragment.java +++ b/src/main/java/com/owncloud/android/ui/fragment/ShareFileFragment.java @@ -4,17 +4,19 @@ * @author masensio * @author David A. Velasco * @author Juan Carlos González Cabrero + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. - *

+ * Copyright (C) 2018 Andy Scherzinger + * * 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 . */ @@ -40,7 +42,6 @@ import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ListAdapter; import android.widget.ListView; import android.widget.ScrollView; import android.widget.TextView; @@ -651,7 +652,6 @@ private void updateListOfUserGroups() { noShares.setVisibility(View.GONE); usersList.setVisibility(View.VISIBLE); usersList.setAdapter(mUserGroupsAdapter); - setListViewHeightBasedOnChildren(usersList); } else { noShares.setVisibility(View.VISIBLE); usersList.setVisibility(View.GONE); @@ -846,7 +846,7 @@ public void onClick(View v) { } - /// BEWARE: next methods will failed with NullPointerException if called before onCreateView() finishes + // BEWARE: following methods will fail with NullPointerException if called before onCreateView() finishes private SwitchCompat getShareViaLinkSwitch() { return (SwitchCompat) getView().findViewById(R.id.shareViaLinkSectionSwitch); @@ -908,29 +908,6 @@ private void hidePublicShare() { getHideFileListingPermissionSection().setVisibility(View.GONE); } - public static void setListViewHeightBasedOnChildren(ListView listView) { - ListAdapter listAdapter = listView.getAdapter(); - if (listAdapter == null) { - return; - } - int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.AT_MOST); - int totalHeight = 0; - View view = null; - for (int i = 0; i < listAdapter.getCount(); i++) { - view = listAdapter.getView(i, view, listView); - if (i == 0) { - view.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth, ViewGroup.LayoutParams.WRAP_CONTENT)); - } - view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED); - totalHeight += view.getMeasuredHeight(); - } - ViewGroup.LayoutParams params = listView.getLayoutParams(); - params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); - listView.setLayoutParams(params); - listView.requestLayout(); - } - - /** * Starts a dialog that requests a password to the user to protect a share link. * diff --git a/src/main/java/com/owncloud/android/ui/fragment/util/FileDetailSharingFragmentHelper.java b/src/main/java/com/owncloud/android/ui/fragment/util/FileDetailSharingFragmentHelper.java new file mode 100644 index 000000000000..d01a1a906274 --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/fragment/util/FileDetailSharingFragmentHelper.java @@ -0,0 +1,136 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * Copyright (C) 2018 Andy Scherzinger + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + */ + +package com.owncloud.android.ui.fragment.util; + +import android.app.SearchManager; +import android.content.ComponentName; +import android.content.res.Resources; +import android.support.v7.widget.SearchView; +import android.view.MenuItem; +import android.view.inputmethod.EditorInfo; + +import com.owncloud.android.R; +import com.owncloud.android.lib.resources.shares.OCShare; +import com.owncloud.android.lib.resources.status.OCCapability; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Helper calls for visibility logic of the sharing fragment. + */ +public class FileDetailSharingFragmentHelper { + + /** + * Sets checked/visiblity state on the given {@link MenuItem} based on the given criteria. + * + * @param fileListing the {@link MenuItem} to be setup + * @param isFolder flag if it is a folder + * @param isEditingAllowed flag if editing is allowed + * @param publicSharePermissions share permissions of the link + */ + public static void setupHideFileListingMenuItem(MenuItem fileListing, + boolean isFolder, + boolean isEditingAllowed, + int publicSharePermissions) { + if (!isFolder) { + fileListing.setVisible(false); + } else { + if (isEditingAllowed) { + boolean readOnly = (publicSharePermissions & OCShare.READ_PERMISSION_FLAG) != 0; + fileListing.setChecked(!readOnly); + } else { + fileListing.setVisible(false); + } + } + } + + /** + * sets up the password {@link MenuItem}'s title based on the fact if a password is present. + * + * @param password the password {@link MenuItem} + * @param isPasswordProtected flag is a password is present + */ + public static void setupPasswordMenuItem(MenuItem password, boolean isPasswordProtected) { + if (isPasswordProtected) { + password.setTitle(R.string.share_password_title); + } else { + password.setTitle(R.string.share_no_password_title); + } + } + + /** + * sets up the expiration date {@link MenuItem}'s title based on the fact if an expiration date is present. + * + * @param expirationDate the expiration date {@link MenuItem} + * @param expirationDateValue the expiration date + * @param res Resources to load the corresponding strings. + */ + public static void setupExpirationDateMenuItem(MenuItem expirationDate, long expirationDateValue, Resources res) { + if (expirationDateValue > 0) { + expirationDate.setTitle(res.getString( + R.string.share_expiration_date_label, + SimpleDateFormat.getDateInstance().format(new Date(expirationDateValue)) + )); + } else { + expirationDate.setTitle(R.string.share_no_expiration_date_label); + } + } + + /** + * sets up the {@link SearchView}. + * + * @param searchManager the {@link SearchManager} + * @param searchView the {@link SearchView} + * @param componentName the {@link ComponentName} + */ + public static void setupSearchView(SearchManager searchManager, SearchView searchView, ComponentName componentName) { + // assumes parent activity is the searchable activity + searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)); + + // do not iconify the widget; expand it by default + searchView.setIconifiedByDefault(false); + + // avoid fullscreen with softkeyboard + searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + // return true to prevent the query from being processed; + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + // leave it for the parent listener in the hierarchy / default behaviour + return false; + } + }); + } + + /** + * @return 'True' when public share is disabled in the server. + */ + public static boolean isPublicShareDisabled(OCCapability capabilities) { + return (capabilities != null && capabilities.getFilesSharingPublicEnabled().isFalse()); + } +} diff --git a/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index 0ba7ae18d8c2..a28671ba905f 100755 --- a/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -4,7 +4,9 @@ * @author masensio * @author David A. Velasco * @author Juan Carlos González Cabrero + * @author Andy Scherzinger * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -83,7 +85,7 @@ import java.util.regex.Pattern; /** - * + * Helper implementation for file operations locally and remote. */ public class FileOperationsHelper { @@ -537,6 +539,29 @@ public void setExpirationDateToShareViaLink(OCFile file, long expirationTimeInMi queueShareIntent(updateShareIntent); } + /** + * Updates a public share on a file to set its expiration date. + * Starts a request to do it in {@link OperationsService} + * + * @param share {@link OCShare} instance which permissions will be updated. + * @param expirationTimeInMillis Expiration date to set. A negative value clears the current expiration + * date, leaving the link unrestricted. Zero makes no change. + */ + public void setExpirationDateToShare(OCShare share, long expirationTimeInMillis) { + Intent updateShareIntent = new Intent(mFileActivity, OperationsService.class); + updateShareIntent.setAction(OperationsService.ACTION_UPDATE_SHARE); + updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); + updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); + updateShareIntent.putExtra( + OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, + expirationTimeInMillis + ); + updateShareIntent.putExtra( + OperationsService.EXTRA_SHARE_PERMISSIONS, + 0 + ); + queueShareIntent(updateShareIntent); + } /** * Updates a share on a file to set its access permissions. @@ -615,17 +640,21 @@ public boolean isSearchUserSupportedSupported() { return false; } - public void sendShareFile(OCFile file) { + public void sendShareFile(OCFile file, boolean hideNcSharingOptions) { // Show dialog FragmentManager fm = mFileActivity.getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); ft.addToBackStack(null); - SendShareDialog mSendShareDialog = SendShareDialog.newInstance(file); + SendShareDialog mSendShareDialog = SendShareDialog.newInstance(file, hideNcSharingOptions); mSendShareDialog.setFileOperationsHelper(this); mSendShareDialog.show(ft, "TAG_SEND_SHARE_DIALOG"); } + public void sendShareFile(OCFile file) { + sendShareFile(file, false); + } + public void syncFiles(Collection files) { for (OCFile file : files) { syncFile(file); @@ -709,7 +738,6 @@ public void syncFile(OCFile file) { intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); mFileActivity.startService(intent); - } } diff --git a/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java b/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java index ec122ddb10be..587efba70a67 100644 --- a/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java +++ b/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java @@ -312,6 +312,11 @@ public void showDetails(OCFile file) { finish(); } + @Override + public void showDetails(OCFile file, int activeTab) { + showDetails(file); + } + public void requestForDownload(OCFile file) { if (mDownloaderBinder == null) { Log_OC.d(TAG, "requestForDownload called without binder to download service"); diff --git a/src/main/java/com/owncloud/android/utils/BitmapUtils.java b/src/main/java/com/owncloud/android/utils/BitmapUtils.java index 982b4034c42a..80d8081bd109 100644 --- a/src/main/java/com/owncloud/android/utils/BitmapUtils.java +++ b/src/main/java/com/owncloud/android/utils/BitmapUtils.java @@ -270,7 +270,7 @@ private static float HueToRGB(float p, float q, float h) { * @param name The name * @return corresponding RGB color * @throws UnsupportedEncodingException if the charset is not supported - * @throws NoSuchAlgorithmException if the specified algorithm is not available + * @throws NoSuchAlgorithmException if the specified algorithm is not available */ public static int[] calculateHSL(String name) throws UnsupportedEncodingException, NoSuchAlgorithmException { // using adapted algorithm from https://github.com/nextcloud/server/blob/master/core/js/placeholder.js#L126 @@ -375,6 +375,10 @@ public static String md5(String string) { * @return the circular bitmap */ public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources, Bitmap bitmap) { + if (bitmap == null) { + return null; + } + RoundedBitmapDrawable roundedBitmap = RoundedBitmapDrawableFactory.create(resources, bitmap); roundedBitmap.setCircular(true); return roundedBitmap; diff --git a/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/src/main/java/com/owncloud/android/utils/DisplayUtils.java index d821573bd092..1a690248dc25 100644 --- a/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -25,6 +25,7 @@ package com.owncloud.android.utils; import android.accounts.Account; +import android.accounts.AccountManager; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -433,50 +434,72 @@ public interface AvatarGenerationListener { } /** - * fetches and sets the avatar of the current account in the drawer in case the drawer is available. + * fetches and sets the avatar of the given account in the passed callContext * - * @param account the account to be set in the drawer + * @param account the account to be used to connect to server * @param avatarRadius the avatar radius * @param resources reference for density information * @param storageManager reference for caching purposes + * @param callContext which context is called to set the generated avatar */ - public static void setAvatar(Account account, AvatarGenerationListener listener, float avatarRadius, - Resources resources, FileDataStorageManager storageManager, Object callContext) { - if (account != null) { - if (callContext instanceof View) { - ((View) callContext).setContentDescription(account.name); - } + public static void setAvatar(@NonNull Account account, AvatarGenerationListener listener, + float avatarRadius, Resources resources, FileDataStorageManager storageManager, + Object callContext, Context context) { + + AccountManager accountManager = AccountManager.get(context); + String userId = accountManager.getUserData(account, + com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); + + setAvatar(account, userId, listener, avatarRadius, resources, storageManager, callContext, context); + } + + /** + * fetches and sets the avatar of the given account in the passed callContext + * + * @param account the account to be used to connect to server + * @param userId the userId which avatar should be set + * @param avatarRadius the avatar radius + * @param resources reference for density information + * @param storageManager reference for caching purposes + * @param callContext which context is called to set the generated avatar + */ + public static void setAvatar(@NonNull Account account, @NonNull String userId, AvatarGenerationListener listener, + float avatarRadius, Resources resources, FileDataStorageManager storageManager, + Object callContext, Context context) { + if (callContext instanceof View) { + ((View) callContext).setContentDescription(account.name); + } - ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider( - MainApp.getAppContext().getContentResolver()); + ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver()); - String eTag = arbitraryDataProvider.getValue(account, ThumbnailsCacheManager.AVATAR); + String serverName = account.name.substring(account.name.lastIndexOf('@') + 1, account.name.length()); + String eTag = arbitraryDataProvider.getValue(userId + "@" + serverName, ThumbnailsCacheManager.AVATAR); + String avatarKey = "a_" + userId + "_" + serverName + "_" + eTag; - // first show old one - Drawable avatar = BitmapUtils.bitmapToCircularBitmapDrawable(resources, - ThumbnailsCacheManager.getBitmapFromDiskCache("a_" + account.name + "_" + eTag)); + // first show old one + Drawable avatar = BitmapUtils.bitmapToCircularBitmapDrawable(resources, + ThumbnailsCacheManager.getBitmapFromDiskCache(avatarKey)); - // if no one exists, show colored icon with initial char - if (avatar == null) { - try { - avatar = TextDrawable.createAvatar(account.name, avatarRadius); - } catch (Exception e) { - Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e); - avatar = resources.getDrawable(R.drawable.account_circle_white); - } + // if no one exists, show colored icon with initial char + if (avatar == null) { + try { + avatar = TextDrawable.createAvatarByUserId(userId, avatarRadius); + } catch (Exception e) { + Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e); + avatar = resources.getDrawable(R.drawable.account_circle_white); } + } - // check for new avatar, eTag is compared, so only new one is downloaded - if (ThumbnailsCacheManager.cancelPotentialAvatarWork(account.name, callContext)) { - final ThumbnailsCacheManager.AvatarGenerationTask task = - new ThumbnailsCacheManager.AvatarGenerationTask(listener, callContext, storageManager, - account, resources, avatarRadius); + // check for new avatar, eTag is compared, so only new one is downloaded + if (ThumbnailsCacheManager.cancelPotentialAvatarWork(userId, callContext)) { + final ThumbnailsCacheManager.AvatarGenerationTask task = + new ThumbnailsCacheManager.AvatarGenerationTask(listener, callContext, storageManager, + account, resources, avatarRadius, userId, serverName, context); - final ThumbnailsCacheManager.AsyncAvatarDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncAvatarDrawable(resources, avatar, task); - listener.avatarGenerated(asyncDrawable, callContext); - task.execute(account.name); - } + final ThumbnailsCacheManager.AsyncAvatarDrawable asyncDrawable = + new ThumbnailsCacheManager.AsyncAvatarDrawable(resources, avatar, task); + listener.avatarGenerated(asyncDrawable, callContext); + task.execute(userId); } } diff --git a/src/main/java/com/owncloud/android/utils/ThemeUtils.java b/src/main/java/com/owncloud/android/utils/ThemeUtils.java index 2f29aa72b010..c69dcd6308c6 100644 --- a/src/main/java/com/owncloud/android/utils/ThemeUtils.java +++ b/src/main/java/com/owncloud/android/utils/ThemeUtils.java @@ -4,8 +4,8 @@ * @author Tobias Kaminsky * @author Andy Scherzinger * Copyright (C) 2017 Tobias Kaminsky - * Copyright (C) 2017 Andy Scherzinger - * Copyright (C) 2017 Nextcloud GmbH. + * Copyright (C) 2017 Nextcloud GmbH + * Copyright (C) 2018 Andy Scherzinger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -23,6 +23,7 @@ package com.owncloud.android.utils; import android.accounts.Account; +import android.app.Activity; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; @@ -212,6 +213,12 @@ public static String getDefaultDisplayNameForRootFolder(Context context) { } } + public static void setStatusBarColor(Activity activity, @ColorInt int color) { + if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().setStatusBarColor(color); + } + } + /** * Adjust lightness of given color * diff --git a/src/main/res/drawable/divider.xml b/src/main/res/drawable/divider.xml new file mode 100644 index 000000000000..2161c98a960e --- /dev/null +++ b/src/main/res/drawable/divider.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/src/main/res/drawable/ic_alert_octagon.xml b/src/main/res/drawable/ic_alert_octagon.xml new file mode 100644 index 000000000000..8f739bacd2e3 --- /dev/null +++ b/src/main/res/drawable/ic_alert_octagon.xml @@ -0,0 +1,23 @@ + + + + diff --git a/src/main/res/layout/file_details_empty.xml b/src/main/res/layout/file_details_empty.xml deleted file mode 100644 index 3df52a590833..000000000000 --- a/src/main/res/layout/file_details_empty.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/res/layout/file_details_fragment.xml b/src/main/res/layout/file_details_fragment.xml index 9804d97e26e7..4ed63cb02b51 100644 --- a/src/main/res/layout/file_details_fragment.xml +++ b/src/main/res/layout/file_details_fragment.xml @@ -4,6 +4,7 @@ Copyright (C) 2012 Bartek Przybylski Copyright (C) 2017 ownCloud GmbH. + Copyright (C) 2018 Andy Scherzinger This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2, @@ -19,7 +20,7 @@ --> @@ -29,27 +30,13 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - - - + android:textStyle="bold" + android:textSize="20sp" /> + android:paddingTop="@dimen/standard_eigth_padding"> - + android:id="@+id/searchView" + android:hint="@string/share_search" + android:layout_marginStart="@dimen/standard_eighth_margin" + android:layout_marginLeft="@dimen/standard_eighth_margin" + android:layout_marginEnd="@dimen/standard_margin" + android:layout_marginRight="@dimen/standard_margin" + style="@style/ownCloud.SearchView"/> + android:paddingTop="@dimen/standard_padding"> - + - + android:layout_gravity="start|center" + android:textColor="@color/black" + android:layout_weight="1" + android:textSize="16sp" + android:paddingTop="@dimen/standard_half_padding" + android:paddingBottom="@dimen/standard_half_padding" + android:text="@string/share_via_link_section_title" /> - + + + + - + android:divider="@drawable/divider" + android:dividerHeight="1dp"/> + android:textSize="16sp" /> diff --git a/src/main/res/layout/share_file_layout.xml b/src/main/res/layout/share_file_layout.xml index 21ad6bccfece..c4ac306a6af9 100644 --- a/src/main/res/layout/share_file_layout.xml +++ b/src/main/res/layout/share_file_layout.xml @@ -169,7 +169,7 @@ android:id="@+id/shareViaLinkEditPermissionSection" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="invisible" + android:visibility="gone" android:layout_marginBottom="@dimen/standard_half_margin" > @@ -203,8 +203,8 @@ android:id="@+id/shareViaLinkHideFileListingPermissionSection" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" android:layout_marginBottom="@dimen/standard_half_margin" - android:visibility="invisible" > diff --git a/src/main/res/menu/file_detail_sharing_link_menu.xml b/src/main/res/menu/file_detail_sharing_link_menu.xml new file mode 100644 index 000000000000..5d41d9d4ac10 --- /dev/null +++ b/src/main/res/menu/file_detail_sharing_link_menu.xml @@ -0,0 +1,42 @@ + + +

+ + + + + + diff --git a/src/main/res/menu/file_detail_sharing_menu.xml b/src/main/res/menu/file_detail_sharing_menu.xml new file mode 100644 index 000000000000..7f05cc42ac7c --- /dev/null +++ b/src/main/res/menu/file_detail_sharing_menu.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/src/main/res/menu/file_details_actions_menu.xml b/src/main/res/menu/file_details_actions_menu.xml new file mode 100644 index 000000000000..7b7f49ac247d --- /dev/null +++ b/src/main/res/menu/file_details_actions_menu.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/values/dims.xml b/src/main/res/values/dims.xml index f2700b298106..517be6899d70 100644 --- a/src/main/res/values/dims.xml +++ b/src/main/res/values/dims.xml @@ -53,6 +53,7 @@ 0dp 72dp 40dp + 20dp 56dp 10dp 5dp @@ -101,9 +102,6 @@ 72dp 72dp 72dp - 14dp - 14dp - 14sp 32dp 32dp 16sp diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6e521dd4c5f6..e3ee9fd23236 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -113,7 +113,6 @@ No uploads available Upload some content or activate auto upload. folder - Tap on a file to display additional information. Download Sync File renamed %1$s during upload @@ -132,6 +131,7 @@ Unknown error Pending Delete + Send About Remove account Remove account %s and delete all local files?\n\nDeletion cannot be undone. @@ -478,17 +478,20 @@ Share with users and groups No data shared with users yet Add user or group + Password protect (%1$s) + Expires %1$s + Set expiration date Share link - Set expiration date - Protect with password - Secured - Allow edits + Password-protected + Set password + edit Hide file listing Get link Share with… Share with %1$s + Unset - Search + Name, federated cloud ID or email address… Search users and groups %1$s (group) @@ -497,14 +500,12 @@ %1$s ( at %2$s ) Upgrade the server version to allow sharing between users from within their clients.\nPlease contact your admin - Share by link enabled - Not shared by link - Users and groups with access + Unshare can share can edit - create - change - delete + can create + can change + can delete Retry failed uploads Clear failed uploads @@ -784,4 +785,7 @@ Create new folder Virus detected. Upload cannot be completed! Tags + Adding sharee failed + Unsharing failed + Updating share failed diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index ff529779ec32..fbfff9279804 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -39,6 +39,12 @@ #757575 + + + +