浏览代码

Merge pull request #11059 from nextcloud/feature/3175/shortcuts_bottombar

Allow Pin to Home with file actions bottom sheet
Álvaro Brey 2 年之前
父节点
当前提交
b0a3d6cc7e

+ 3 - 0
app/src/main/AndroidManifest.xml

@@ -55,6 +55,9 @@
     <uses-permission android:name="android.permission.USE_FINGERPRINT" />
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
+    <uses-permission
+        android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
+        android:maxSdkVersion="25" />
 
     <!-- Apps that target Android 9 (API level 28) or higher and use foreground services
     must request the FOREGROUND_SERVICE permission -->

+ 6 - 2
app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt

@@ -65,7 +65,10 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
 
     // locks
     UNLOCK_FILE(R.id.action_unlock_file, R.string.unlock_file, R.drawable.ic_lock_open_white),
-    LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_lock);
+    LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_lock),
+
+    // Shortcuts
+    PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen);
 
     companion object {
         /**
@@ -95,7 +98,8 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
             SET_ENCRYPTED,
             UNSET_ENCRYPTED,
             SET_AS_WALLPAPER,
-            REMOVE_FILE
+            REMOVE_FILE,
+            PIN_TO_HOMESCREEN
         )
     }
 }

+ 121 - 0
app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt

@@ -0,0 +1,121 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Felix Nüsse
+ *
+ * Copyright (C) 2022 Felix Nüsse
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.utils
+
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_IMMUTABLE
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import androidx.core.graphics.drawable.toBitmap
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.utils.MimeTypeUtil
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+class ShortcutUtil @Inject constructor(private val mContext: Context) {
+
+    /**
+     * Adds a pinned shortcut to the home screen that points to the passed file/folder.
+     *
+     * @param file The file/folder to which a pinned shortcut should be added to the home screen.
+     */
+    fun addShortcutToHomescreen(file: OCFile, viewThemeUtils: ViewThemeUtils) {
+        if (ShortcutManagerCompat.isRequestPinShortcutSupported(mContext)) {
+            val intent = Intent(mContext, FileDisplayActivity::class.java)
+            intent.action = FileDisplayActivity.OPEN_FILE
+            intent.putExtra(FileActivity.EXTRA_FILE, file.remotePath)
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+            val shortcutId = "nextcloud_shortcut_" + file.remoteId
+            val icon: IconCompat
+            var thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
+                ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
+            )
+            if (thumbnail != null) {
+                thumbnail = bitmapToAdaptiveBitmap(thumbnail)
+                icon = IconCompat.createWithAdaptiveBitmap(thumbnail)
+            } else if (file.isFolder) {
+                val bitmapIcon = MimeTypeUtil.getFolderTypeIcon(
+                    file.isSharedWithMe || file.isSharedWithSharee,
+                    file.isSharedViaLink,
+                    file.isEncrypted,
+                    file.isGroupFolder,
+                    file.mountType,
+                    mContext,
+                    viewThemeUtils
+                ).toBitmap()
+                icon = IconCompat.createWithBitmap(bitmapIcon)
+            } else {
+                icon = IconCompat.createWithResource(
+                    mContext,
+                    MimeTypeUtil.getFileTypeIconId(file.mimeType, file.fileName)
+                )
+            }
+            val longLabel = mContext.getString(R.string.pin_shortcut_label, file.fileName)
+            val pinShortcutInfo = ShortcutInfoCompat.Builder(mContext, shortcutId)
+                .setShortLabel(file.fileName)
+                .setLongLabel(longLabel)
+                .setIcon(icon)
+                .setIntent(intent)
+                .build()
+            val pinnedShortcutCallbackIntent =
+                ShortcutManagerCompat.createShortcutResultIntent(mContext, pinShortcutInfo)
+            val successCallback = PendingIntent.getBroadcast(
+                mContext,
+                0,
+                pinnedShortcutCallbackIntent,
+                FLAG_IMMUTABLE
+            )
+            ShortcutManagerCompat.requestPinShortcut(
+                mContext,
+                pinShortcutInfo,
+                successCallback.intentSender
+            )
+        }
+    }
+
+    private fun bitmapToAdaptiveBitmap(orig: Bitmap): Bitmap {
+        val adaptiveIconSize = mContext.resources.getDimensionPixelSize(R.dimen.adaptive_icon_size)
+        val adaptiveIconOuterSides = mContext.resources.getDimensionPixelSize(R.dimen.adaptive_icon_padding)
+        val drawable: Drawable = BitmapDrawable(mContext.resources, orig)
+        val bitmap = Bitmap.createBitmap(adaptiveIconSize, adaptiveIconSize, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(bitmap)
+        drawable.setBounds(
+            adaptiveIconOuterSides,
+            adaptiveIconOuterSides,
+            adaptiveIconSize - adaptiveIconOuterSides,
+            adaptiveIconSize - adaptiveIconOuterSides
+        )
+        drawable.draw(canvas)
+        return bitmap
+    }
+}

+ 8 - 0
app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -51,6 +51,7 @@ import java.util.List;
 import javax.inject.Inject;
 
 import androidx.annotation.IdRes;
+import androidx.core.content.pm.ShortcutManagerCompat;
 
 /**
  * Filters out the file actions available in a given {@link Menu} for a given {@link OCFile}
@@ -177,6 +178,7 @@ public class FileMenuFilter {
         filterStream(toHide);
         filterLock(toHide, fileLockingEnabled);
         filterUnlock(toHide, fileLockingEnabled);
+        filterPinToHome(toHide);
 
         return toHide;
     }
@@ -262,6 +264,12 @@ public class FileMenuFilter {
         }
     }
 
+    private void filterPinToHome(List<Integer> toHide) {
+        if (!isSingleSelection() || !ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
+            toHide.add(R.id.action_pin_to_homescreen);
+        }
+    }
+
     private void filterEdit(
         List<Integer> toHide,
         OCCapability capability

+ 25 - 0
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -165,6 +165,8 @@ public class FileDisplayActivity extends FileActivity
     public static final String ALL_FILES = "ALL_FILES";
     public static final String PHOTO_SEARCH = "PHOTO_SEARCH";
     public static final int SINGLE_USER_SIZE = 1;
+    public static final String OPEN_FILE = "NC_OPEN_FILE";
+
 
     private FilesBinding binding;
 
@@ -359,6 +361,11 @@ public class FileDisplayActivity extends FileActivity
             syncAndUpdateFolder(true);
         }
 
+        if (OPEN_FILE.equals(getIntent().getAction())) {
+            getSupportFragmentManager().executePendingTransactions();
+            onOpenFileIntent(getIntent());
+        }
+
         upgradeNotificationForInstantUpload();
         checkOutdatedServer();
     }
@@ -504,6 +511,8 @@ public class FileDisplayActivity extends FileActivity
             showDetails(file);
         } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
             handleOpenFileViaIntent(intent);
+        } else if (OPEN_FILE.equals(intent.getAction())) {
+            onOpenFileIntent(intent);
         } else if (RESTART.equals(intent.getAction())) {
             finish();
             startActivity(intent);
@@ -547,6 +556,22 @@ public class FileDisplayActivity extends FileActivity
             }
     }
 
+    private void onOpenFileIntent(Intent intent) {
+        String extra = intent.getStringExtra(EXTRA_FILE);
+        OCFile file = getStorageManager().getFileByDecryptedRemotePath(extra);
+        if (file != null) {
+            OCFileListFragment fileFragment;
+            final Fragment leftFragment = getLeftFragment();
+            if (leftFragment instanceof OCFileListFragment) {
+                fileFragment = (OCFileListFragment) leftFragment;
+            } else {
+                fileFragment = new OCFileListFragment();
+                setLeftFragment(fileFragment);
+            }
+            fileFragment.onItemClicked(file);
+        }
+    }
+
     /**
      * Replaces the first fragment managed by the activity with the received as a parameter.
      *

+ 5 - 1
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -58,6 +58,7 @@ import com.nextcloud.client.utils.Throttler;
 import com.nextcloud.common.NextcloudClient;
 import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
 import com.nextcloud.utils.EditorUtils;
+import com.nextcloud.utils.ShortcutUtil;
 import com.nextcloud.utils.view.FastScrollUtils;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
@@ -130,7 +131,6 @@ import androidx.annotation.StringRes;
 import androidx.appcompat.app.ActionBar;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 import androidx.core.content.ContextCompat;
-import androidx.core.content.res.ResourcesCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
 import androidx.fragment.app.FragmentActivity;
 import androidx.fragment.app.FragmentManager;
@@ -198,6 +198,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     @Inject ViewThemeUtils viewThemeUtils;
     @Inject FastScrollUtils fastScrollUtils;
     @Inject EditorUtils editorUtils;
+    @Inject ShortcutUtil shortcutUtil;
 
     protected FileFragment.ContainerActivity mContainerActivity;
 
@@ -1147,6 +1148,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 mContainerActivity.getFileOperationsHelper().toggleFileLock(singleFile, true);
             } else if (itemId == R.id.action_unlock_file) {
                 mContainerActivity.getFileOperationsHelper().toggleFileLock(singleFile, false);
+            } else if (itemId == R.id.action_pin_to_homescreen) {
+                shortcutUtil.addShortcutToHomescreen(singleFile, viewThemeUtils);
+                return true;
             }
         }
 

+ 25 - 0
app/src/main/res/drawable/add_to_home_screen.xml

@@ -0,0 +1,25 @@
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2022 Nextcloud.
+
+  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 <http://www.gnu.org/licenses/>.
+
+  Icon provided by Android Material Library in Apache License 2.0
+-->
+<vector android:autoMirrored="true" android:height="24dp"
+    android:tint="#000000" android:viewportHeight="24"
+    android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M18,1.01L8,1c-1.1,0 -2,0.9 -2,2v3c0,0.55 0.45,1 1,1s1,-0.45 1,-1V5h10v14H8v-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM11,15c0.55,0 1,-0.45 1,-1V9c0,-0.55 -0.45,-1 -1,-1H6c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h2.59L3.7,14.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L10,11.41V14c0,0.55 0.45,1 1,1z"/>
+</vector>

+ 3 - 0
app/src/main/res/values/dims.xml

@@ -148,4 +148,7 @@
     <dimen name="dialog_padding">24dp</dimen>
     <integer name="small_margin">5</integer>
     <integer name="zero">0</integer>
+    <!--Adaptive Icon size specified here:  https://developer.android.com/develop/ui/views/launch/icon_design_adaptive -->
+    <dimen name="adaptive_icon_size">108dp</dimen>
+    <dimen name="adaptive_icon_padding">18dp</dimen>
 </resources>

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

@@ -46,4 +46,5 @@
     <item name="action_unset_encrypted" type="id"/>
     <item name="action_set_as_wallpaper" type="id"/>
     <item name="action_remove_file" type="id"/>
+    <item name="action_pin_to_homescreen" type="id"/>
 </resources>

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

@@ -1054,4 +1054,6 @@
     <string name="check_back_later_or_reload">Check back later or reload.</string>
     <string name="e2e_not_yet_setup">E2E not yet setup</string>
     <string name="error_file_actions">Error showing file actions</string>
+    <string name="pin_home">Pin to Home screen</string>
+    <string name="pin_shortcut_label">Open %1$s</string>
 </resources>