Browse Source

Merge branch 'master' into bugfix/make_cancel_work_with_new_upload_worker

Jonas Mayer 1 year ago
parent
commit
4084163ec0

BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png


BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png


+ 0 - 2
app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt

@@ -26,7 +26,6 @@ import androidx.test.espresso.Espresso
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.action.ViewActions.click
 import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
-import androidx.test.espresso.action.ViewActions.scrollTo
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.contrib.DrawerActions
 import androidx.test.espresso.contrib.NavigationViewActions
@@ -241,7 +240,6 @@ class FileDisplayActivityIT : AbstractOnServerIT() {
 
         // browse into folder
         onView(withId(R.id.list_root))
-            .perform(scrollTo())
             .perform(closeSoftKeyboard())
             .perform(
                 RecyclerViewActions.actionOnItemAtPosition<OCFileListItemViewHolder>(

+ 3 - 3
app/src/androidTest/java/com/owncloud/android/UploadIT.java

@@ -460,7 +460,7 @@ public class UploadIT extends AbstractOnServerIT {
         testOnlyOnServer(NextcloudVersion.nextcloud_27);
 
         File file = getFile("gps.jpg");
-        String remotePath = "/gps.jpg";
+        String remotePath = "/metadata.jpg";
         OCUpload ocUpload = new OCUpload(file.getAbsolutePath(), remotePath, account.name);
 
         assertTrue(
@@ -497,7 +497,7 @@ public class UploadIT extends AbstractOnServerIT {
 
         OCFile ocFile = null;
         for (OCFile f : files) {
-            if (f.getFileName().equals("gps.jpg")) {
+            if (f.getFileName().equals("metadata.jpg")) {
                 ocFile = f;
                 break;
             }
@@ -505,8 +505,8 @@ public class UploadIT extends AbstractOnServerIT {
 
         assertNotNull(ocFile);
         assertEquals(remotePath, ocFile.getRemotePath());
-        assertEquals(new ImageDimension(300f, 200f), ocFile.getImageDimension());
         assertEquals(new GeoLocation(64, -46), ocFile.getGeoLocation());
+        assertEquals(new ImageDimension(300f, 200f), ocFile.getImageDimension());
     }
 
     private void verifyStoragePath(OCFile file) {

+ 11 - 4
app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java

@@ -457,16 +457,23 @@ public class InputStreamBinder extends IInputStreamService.Stub {
     }
 
     private boolean isValid(NextcloudRequest request) {
-        String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid());
+        String[] callingPackageNames = context.getPackageManager().getPackagesForUid(Binder.getCallingUid());
 
         SharedPreferences sharedPreferences = context.getSharedPreferences(SSO_SHARED_PREFERENCE,
                                                                            Context.MODE_PRIVATE);
-        String hash = sharedPreferences.getString(callingPackageName + DELIMITER + request.getAccountName(), "");
-        return validateToken(hash, request.getToken());
+        for (String callingPackageName : callingPackageNames) {
+            String hash = sharedPreferences.getString(callingPackageName + DELIMITER + request.getAccountName(), "");
+            if (hash.isEmpty())
+                continue;
+            if (validateToken(hash, request.getToken())) {
+                return true;
+            }
+        }
+        return false;
     }
 
     private boolean validateToken(String hash, String token) {
-        if (hash.isEmpty() || !hash.contains("$")) {
+        if (!hash.contains("$")) {
             throw new IllegalStateException(EXCEPTION_INVALID_TOKEN);
         }
 

+ 10 - 16
app/src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.java → app/src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.kt

@@ -18,26 +18,20 @@
  *   You should have received a copy of the GNU General Public License
  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
+package com.owncloud.android.ui.activity
 
-package com.owncloud.android.ui.activity;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-import com.owncloud.android.utils.ClipboardUtil;
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import com.owncloud.android.utils.ClipboardUtil
 
 /**
  * Activity copying the text of the received Intent into the system clipboard.
  */
-public class CopyToClipboardActivity extends Activity {
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        ClipboardUtil.copyToClipboard(this, getIntent().getCharSequenceExtra(Intent.EXTRA_TEXT).toString());
-
-        finish();
+class CopyToClipboardActivity : Activity() {
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        ClipboardUtil.copyToClipboard(this, intent.getCharSequenceExtra(Intent.EXTRA_TEXT).toString())
+        finish()
     }
 }

+ 101 - 13
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java

@@ -7,7 +7,7 @@
  *
  * Copyright (C) 2018 Andy Scherzinger
  * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
- * Copyright (C) 2020 TSI-mc
+ * Copyright (C) 2023 TSI-mc
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@@ -25,11 +25,17 @@
 
 package com.owncloud.android.ui.fragment;
 
+import android.Manifest;
 import android.accounts.AccountManager;
+import android.app.Activity;
 import android.app.SearchManager;
 import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
 import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.os.Bundle;
+import android.provider.ContactsContract;
 import android.text.InputType;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
@@ -46,6 +52,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+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.lib.resources.status.NextcloudVersion;
@@ -61,6 +68,7 @@ import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper;
 import com.owncloud.android.ui.helpers.FileOperationsHelper;
 import com.owncloud.android.utils.ClipboardUtil;
 import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.PermissionUtil;
 import com.owncloud.android.utils.theme.ViewThemeUtils;
 
 import java.util.ArrayList;
@@ -68,6 +76,8 @@ import java.util.List;
 
 import javax.inject.Inject;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -81,7 +91,6 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
 
     private static final String ARG_FILE = "FILE";
     private static final String ARG_USER = "USER";
-    public static final int PERMISSION_EDITING_ALLOWED = 17;
 
     private OCFile file;
     private User user;
@@ -118,8 +127,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
         } else {
             Bundle arguments = getArguments();
             if (arguments != null) {
-                file = getArguments().getParcelable(ARG_FILE);
-                user = getArguments().getParcelable(ARG_USER);
+                file = arguments.getParcelable(ARG_FILE);
+                user = arguments.getParcelable(ARG_USER);
             }
         }
 
@@ -149,12 +158,11 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         binding = FileDetailsSharingFragmentBinding.inflate(inflater, container, false);
-        View view = binding.getRoot();
 
         fileOperationsHelper = fileActivity.getFileOperationsHelper();
         fileDataStorageManager = fileActivity.getStorageManager();
 
-        AccountManager accountManager = AccountManager.get(getContext());
+        AccountManager accountManager = AccountManager.get(requireContext());
         String userId = accountManager.getUserData(user.toPlatformAccount(),
                                                    com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
 
@@ -165,11 +173,14 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
                                                             user,
                                                             viewThemeUtils,
                                                             file.isEncrypted()));
-        binding.sharesList.setLayoutManager(new LinearLayoutManager(getContext()));
+
+        binding.sharesList.setLayoutManager(new LinearLayoutManager(requireContext()));
+
+        binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission());
 
         setupView();
 
-        return view;
+        return binding.getRoot();
     }
 
     @Override
@@ -208,6 +219,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
             } else {
                 binding.searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed));
                 binding.searchView.setInputType(InputType.TYPE_NULL);
+                binding.pickContactEmailBtn.setVisibility(View.GONE);
                 disableSearchView(binding.searchView);
             }
         }
@@ -216,9 +228,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
     private void disableSearchView(View view) {
         view.setEnabled(false);
 
-        if (view instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) view;
-
+        if (view instanceof ViewGroup viewGroup) {
             for (int i = 0; i < viewGroup.getChildCount(); i++) {
                 disableSearchView(viewGroup.getChildAt(i));
             }
@@ -302,7 +312,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
             if (TextUtils.isEmpty(share.getShareLink())) {
                 fileOperationsHelper.getFileWithLink(file, viewThemeUtils);
             } else {
-                ClipboardUtil.copyToClipboard(getActivity(), share.getShareLink());
+                ClipboardUtil.copyToClipboard(requireActivity(), share.getShareLink());
             }
         }
     }
@@ -460,6 +470,52 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
         adapter.addShares(publicShares);
     }
 
+    private void checkContactPermission() {
+        if (PermissionUtil.checkSelfPermission(requireActivity(), Manifest.permission.READ_CONTACTS)) {
+            pickContactEmail();
+        } else {
+            requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS);
+        }
+    }
+
+    private void pickContactEmail() {
+        Intent intent = new Intent(Intent.ACTION_PICK);
+        intent.setDataAndType(ContactsContract.Contacts.CONTENT_URI, ContactsContract.CommonDataKinds.Email.CONTENT_TYPE);
+        onContactSelectionResultLauncher.launch(intent);
+    }
+
+    private void handleContactResult(@NonNull Uri contactUri) {
+        // Define the projection to get all email addresses.
+        String[] projection = {ContactsContract.CommonDataKinds.Email.ADDRESS};
+
+        Cursor cursor = fileActivity.getContentResolver().query(contactUri, projection, null, null, null);
+
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                // The contact has only one email address, use it.
+                int columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS);
+                if (columnIndex != -1) {
+                    // Use the email address as needed.
+                    // email variable contains the selected contact's email address.
+                    String email = cursor.getString(columnIndex);
+                    binding.searchView.post(() -> {
+                        binding.searchView.setQuery(email, false);
+                        binding.searchView.requestFocus();
+                    });
+                } else {
+                    DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                    Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address.");
+                }
+            } else {
+                DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as no Email found.");
+            }
+            cursor.close();
+        } else {
+            DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+            Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as Cursor is null.");
+        }
+    }
 
     private boolean containsNoNewPublicShare(List<OCShare> shares) {
         for (OCShare share : shares) {
@@ -496,7 +552,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
 
     @VisibleForTesting
     public void search(String query) {
-        SearchView searchView = getView().findViewById(R.id.searchView);
+        SearchView searchView = requireView().findViewById(R.id.searchView);
         searchView.setQuery(query, true);
     }
 
@@ -546,6 +602,38 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
         fileOperationsHelper.setPermissionsToShare(share, permission);
     }
 
+    //launcher for contact permission
+    private final ActivityResultLauncher<String> requestContactPermissionLauncher =
+        registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
+            if (isGranted) {
+                pickContactEmail();
+            } else {
+                DisplayUtils.showSnackMessage(binding.getRoot(), R.string.contact_no_permission);
+            }
+        });
+
+    //launcher to handle contact selection
+    private final ActivityResultLauncher<Intent> onContactSelectionResultLauncher =
+        registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
+                                  result -> {
+                                      if (result.getResultCode() == Activity.RESULT_OK) {
+                                          Intent intent = result.getData();
+                                          if (intent == null) {
+                                              DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                                              return;
+                                          }
+
+                                          Uri contactUri = intent.getData();
+                                          if (contactUri == null) {
+                                              DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                                              return;
+                                          }
+
+                                          handleContactResult(contactUri);
+
+                                      }
+                                  });
+
     public interface OnEditShareListener {
         void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown,
                                boolean isExpiryDateShown);

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt

@@ -545,7 +545,7 @@ class FileDetailsSharingProcessFragment :
         )
         // copy the share link if available
         if (!TextUtils.isEmpty(share?.shareLink)) {
-            ClipboardUtil.copyToClipboard(activity, share?.shareLink)
+            ClipboardUtil.copyToClipboard(requireActivity(), share?.shareLink)
         }
     }
 

+ 28 - 33
app/src/main/java/com/owncloud/android/utils/ClipboardUtil.java → app/src/main/java/com/owncloud/android/utils/ClipboardUtil.kt

@@ -17,51 +17,46 @@
  * 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.owncloud.android.utils
 
-package com.owncloud.android.utils;
-
-import android.app.Activity;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.text.TextUtils;
-import android.widget.Toast;
-
-import com.owncloud.android.R;
-import com.owncloud.android.lib.common.utils.Log_OC;
+import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.text.TextUtils
+import android.widget.Toast
+import com.owncloud.android.R
+import com.owncloud.android.lib.common.utils.Log_OC
 
 /**
  * Helper implementation to copy a string into the system clipboard.
  */
-public final class ClipboardUtil {
-    private static final String TAG = ClipboardUtil.class.getName();
-
-    private ClipboardUtil() {
-    }
+object ClipboardUtil {
+    private val TAG = ClipboardUtil::class.java.name
 
-    public static void copyToClipboard(Activity activity, String text) {
-        copyToClipboard(activity, text, true);
-    }
-
-    public static void copyToClipboard(Activity activity, String text, boolean showToast) {
+    @JvmStatic
+    @JvmOverloads
+    @Suppress("TooGenericExceptionCaught")
+    fun copyToClipboard(activity: Activity, text: String?, showToast: Boolean = true) {
         if (!TextUtils.isEmpty(text)) {
             try {
-                ClipData clip = ClipData.newPlainText(
-                        activity.getString(
-                                R.string.clipboard_label, activity.getString(R.string.app_name)),
-                        text
-                );
-                ((ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(clip);
-
+                val clip = ClipData.newPlainText(
+                    activity.getString(
+                        R.string.clipboard_label,
+                        activity.getString(R.string.app_name)
+                    ),
+                    text
+                )
+                (activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip)
                 if (showToast) {
-                    Toast.makeText(activity, R.string.clipboard_text_copied, Toast.LENGTH_SHORT).show();
+                    Toast.makeText(activity, R.string.clipboard_text_copied, Toast.LENGTH_SHORT).show()
                 }
-            } catch (Exception e) {
-                Toast.makeText(activity, R.string.clipboard_unexpected_error, Toast.LENGTH_SHORT).show();
-                Log_OC.e(TAG, "Exception caught while copying to clipboard", e);
+            } catch (e: Exception) {
+                Toast.makeText(activity, R.string.clipboard_unexpected_error, Toast.LENGTH_SHORT).show()
+                Log_OC.e(TAG, "Exception caught while copying to clipboard", e)
             }
         } else {
-            Toast.makeText(activity, R.string.clipboard_no_text_to_copy, Toast.LENGTH_SHORT).show();
+            Toast.makeText(activity, R.string.clipboard_no_text_to_copy, Toast.LENGTH_SHORT).show()
         }
     }
 }

+ 26 - 0
app/src/main/res/drawable/ic_contact_book.xml

@@ -0,0 +1,26 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2023 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#666666"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z" />
+</vector>

+ 36 - 21
app/src/main/res/layout/file_details_sharing_fragment.xml

@@ -1,7 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?><!--
   Nextcloud Android client application
 
+  @author TSI-mc
+
   Copyright (C) 2018 Andy Scherzinger
+  Copyright (C) 2023 TSI-mc
 
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@@ -29,83 +32,94 @@
         android:id="@+id/search_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:orientation="horizontal"
         android:paddingStart="@dimen/standard_padding"
         android:paddingEnd="@dimen/zero">
 
         <ImageView
             android:id="@+id/searchViewIcon"
-            android:layout_height="@dimen/user_icon_size"
             android:layout_width="@dimen/user_icon_size"
-            android:padding="@dimen/standard_half_padding"
-            android:contentDescription="@string/avatar"
+            android:layout_height="@dimen/user_icon_size"
             android:layout_gravity="center_vertical"
+            android:contentDescription="@string/avatar"
+            android:padding="@dimen/standard_half_padding"
             android:src="@drawable/ic_search_grey" />
 
         <androidx.appcompat.widget.SearchView
             android:id="@+id/searchView"
             style="@style/ownCloud.SearchView"
-            android:layout_width="match_parent"
+            android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_marginStart="@dimen/zero"
             android:layout_marginEnd="@dimen/standard_quarter_margin"
+            android:layout_weight="1"
             android:hint="@string/share_search"
             app:searchIcon="@null" />
 
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/pick_contact_email_btn"
+            android:layout_width="@dimen/minimum_size_for_touchable_area"
+            android:layout_height="@dimen/minimum_size_for_touchable_area"
+            android:layout_gravity="center_vertical"
+            android:padding="12dp"
+            android:layout_marginEnd="@dimen/standard_quarter_margin"
+            android:src="@drawable/ic_contact_book" />
+
     </LinearLayout>
 
     <LinearLayout
         android:id="@+id/shared_with_you_container"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginBottom="@dimen/standard_half_margin"
-        android:layout_width="match_parent"
         android:orientation="horizontal"
         android:paddingLeft="@dimen/standard_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_padding">
+        android:paddingTop="@dimen/standard_padding"
+        android:paddingRight="@dimen/standard_padding">
 
         <ImageView
-            android:contentDescription="@string/avatar"
             android:id="@+id/shared_with_you_avatar"
-            android:layout_height="@dimen/user_icon_size"
             android:layout_width="@dimen/user_icon_size"
+            android:layout_height="@dimen/user_icon_size"
+            android:contentDescription="@string/avatar"
             android:src="@drawable/ic_user" />
 
         <LinearLayout
-            android:layout_height="wrap_content"
             android:layout_width="match_parent"
+            android:layout_height="wrap_content"
             android:orientation="vertical"
             android:paddingLeft="@dimen/standard_padding"
-            android:paddingRight="@dimen/standard_padding"
-            android:paddingTop="@dimen/standard_half_padding">
+            android:paddingTop="@dimen/standard_half_padding"
+            android:paddingRight="@dimen/standard_padding">
 
             <TextView
                 android:id="@+id/shared_with_you_username"
-                android:layout_height="wrap_content"
                 android:layout_width="match_parent"
+                android:layout_height="wrap_content"
                 android:text="@string/shared_with_you_by"
                 android:textSize="@dimen/two_line_primary_text_size" />
 
             <LinearLayout
                 android:id="@+id/shared_with_you_note_container"
-                android:layout_height="match_parent"
                 android:layout_width="match_parent"
+                android:layout_height="match_parent"
                 android:orientation="horizontal"
                 android:paddingTop="@dimen/standard_half_padding"
                 tools:ignore="UseCompoundDrawables">
 
                 <ImageView
-                    android:contentDescription="@string/note_icon_hint"
-                    android:layout_height="16dp"
                     android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:contentDescription="@string/note_icon_hint"
                     android:src="@drawable/file_text" />
 
                 <TextView
                     android:id="@+id/shared_with_you_note"
+                    android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
-                    android:layout_width="0dp"
-                    android:paddingEnd="@dimen/standard_half_padding"
                     android:paddingStart="@dimen/standard_half_padding"
+                    android:paddingEnd="@dimen/standard_half_padding"
                     android:textSize="16sp" />
             </LinearLayout>
 
@@ -113,10 +127,11 @@
     </LinearLayout>
 
     <androidx.recyclerview.widget.RecyclerView
-        android:divider="@drawable/divider"
-        android:dividerHeight="1dp"
         android:id="@+id/sharesList"
+        android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_width="match_parent" />
+        android:divider="@drawable/divider"
+        android:dividerHeight="1dp"
+        tools:listitem="@layout/file_details_share_link_share_item" />
 
 </LinearLayout>

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

@@ -602,6 +602,7 @@
     <string name="actionbar_calendar_contacts_restore">Restore contacts &amp; calendar</string>
     <string name="contacts_backup_button">Back up now</string>
     <string name="contactlist_no_permission">No permission given, nothing imported.</string>
+    <string name="contact_no_permission">Contact permission is required.</string>
     <string name="restore_backup">Restore backup</string>
     <string name="contacts_preferences_no_file_found">No file found</string>
     <string name="contacts_preferences_something_strange_happened">Could not find your last backup!</string>
@@ -939,6 +940,7 @@
     <string name="link_share_file_drop">File drop (upload only)</string>
     <string name="could_not_retrieve_shares">Could not retrieve shares</string>
     <string name="failed_update_ui">Failed to update UI</string>
+    <string name="email_pick_failed">Failed to pick email address.</string>
     <string name="remote">(remote)</string>
     <string name="set_status">Set status</string>
     <string name="online_status">Online status</string>