Add status to sharee

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2020-10-20 06:59:20 +02:00 committed by Andy Scherzinger
parent 153aa77b41
commit dbd9e6fe9b
No known key found for this signature in database
GPG key ID: 6CADC7E3523C308B
24 changed files with 794 additions and 46 deletions

View file

@ -0,0 +1,7 @@
<svg width="24" height="24" enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="none" />
<path
d="m10.615 2.1094c-4.8491 0.68106-8.6152 4.8615-8.6152 9.8906 0 5.5 4.5 10 10 10 5.0292 0 9.2096-3.7661 9.8906-8.6152-1.4654 1.601-3.5625 2.6152-5.8906 2.6152-4.4 0-8-3.6-8-8 0-2.3281 1.0143-4.4252 2.6152-5.8906z"
fill="#f4a331" />
</svg>

After

Width:  |  Height:  |  Size: 449 B

View file

@ -0,0 +1,6 @@
<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none" />
<path d="m12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10z" fill="#ed484c" />
<path d="m8 10h8c1.108 0 2 0.892 2 2s-0.892 2-2 2h-8c-1.108 0-2-0.892-2-2s0.892-2 2-2z" fill="#fdffff"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2" style="paint-order:stroke markers fill" />
</svg>

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -58,7 +58,7 @@ fi
if [[ $4 = "all" ]]; then if [[ $4 = "all" ]]; then
scripts/runAllScreenshotCombinations "noCI" "$1" "-Pandroid.testInstrumentationRunnerArguments.class=$class$method" scripts/runAllScreenshotCombinations "noCI" "$1" "-Pandroid.testInstrumentationRunnerArguments.class=$class$method"
else else
./gradlew gplayDebugExecuteScreenshotTests $record \ ./gradlew --offline gplayDebugExecuteScreenshotTests $record \
-Pandroid.testInstrumentationRunnerArguments.annotation=com.owncloud.android.utils.ScreenshotTest \ -Pandroid.testInstrumentationRunnerArguments.annotation=com.owncloud.android.utils.ScreenshotTest \
-Pandroid.testInstrumentationRunnerArguments.class=$class$method \ -Pandroid.testInstrumentationRunnerArguments.class=$class$method \
$darkMode \ $darkMode \

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -0,0 +1,46 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.providers
import androidx.test.espresso.intent.rule.IntentsTestRule
import com.nextcloud.client.TestActivity
import com.owncloud.android.AbstractOnServerIT
import org.junit.Rule
import org.junit.Test
class UsersAndGroupsSearchProviderIT : AbstractOnServerIT() {
@get:Rule
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
@Test
fun searchUser() {
val activity = testActivityRule.launchActivity(null)
shortSleep()
activity.runOnUiThread {
// fragment.search("Admin")
}
longSleep()
}
}

View file

@ -22,16 +22,21 @@
package com.owncloud.android.ui.fragment package com.owncloud.android.ui.fragment
import android.graphics.BitmapFactory
import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.nextcloud.client.TestActivity import com.nextcloud.client.TestActivity
import com.nextcloud.client.account.StatusType
import com.owncloud.android.AbstractIT import com.owncloud.android.AbstractIT
import com.owncloud.android.R import com.owncloud.android.R
import com.owncloud.android.ui.TextDrawable
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.ScreenshotTest import com.owncloud.android.utils.ScreenshotTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
class AvatarIT : AbstractIT() { class AvatarIT : AbstractIT() {
@get:Rule @get:Rule
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
@ -60,4 +65,124 @@ class AvatarIT : AbstractIT() {
waitForIdleSync() waitForIdleSync()
screenshot(sut) screenshot(sut)
} }
@Test
@ScreenshotTest
fun showAvatarsWithStatus() {
val avatarRadius = targetContext.resources.getDimension(R.dimen.list_item_avatar_icon_radius)
val width = DisplayUtils.convertDpToPixel(2 * avatarRadius, targetContext)
val sut = testActivityRule.launchActivity(null)
val fragment = AvatarTestFragment()
val paulette = BitmapFactory.decodeFile(getFile("paulette.jpg").absolutePath)
val christine = BitmapFactory.decodeFile(getFile("christine.jpg").absolutePath)
val textBitmap = BitmapUtils.drawableToBitmap(TextDrawable.createNamedAvatar("Admin", avatarRadius))
sut.addFragment(fragment)
runOnUiThread {
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(paulette, StatusType.online, "😘", targetContext),
width * 2,
1,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(christine, StatusType.online, "☁️", targetContext),
width * 2,
1,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(christine, StatusType.online, "🌴️", targetContext),
width * 2,
1,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(christine, StatusType.online, "", targetContext),
width * 2,
1,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(paulette, StatusType.dnd, "", targetContext),
width * 2,
1,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(christine, StatusType.away, "", targetContext),
width * 2,
1,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(paulette, StatusType.offline, "", targetContext),
width * 2,
1,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "😘", targetContext),
width,
2,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "☁️", targetContext),
width,
2,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "🌴️", targetContext),
width,
2,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "", targetContext),
width,
2,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.dnd, "", targetContext),
width,
2,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.away, "", targetContext),
width,
2,
targetContext
)
fragment.addBitmap(
BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.offline, "", targetContext),
width,
2,
targetContext
)
}
shortSleep()
waitForIdleSync()
screenshot(sut)
}
} }

View file

@ -22,6 +22,7 @@
package com.owncloud.android.ui.fragment package com.owncloud.android.ui.fragment
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -34,12 +35,14 @@ import com.owncloud.android.R
import com.owncloud.android.ui.TextDrawable import com.owncloud.android.ui.TextDrawable
internal class AvatarTestFragment : Fragment() { internal class AvatarTestFragment : Fragment() {
lateinit var list: LinearLayout lateinit var list1: LinearLayout
lateinit var list2: LinearLayout
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.avatar_fragment, null) val view: View = inflater.inflate(R.layout.avatar_fragment, null)
list = view.findViewById(R.id.avatar_list) list1 = view.findViewById(R.id.avatar_list1)
list2 = view.findViewById(R.id.avatar_list2)
return view return view
} }
@ -54,7 +57,25 @@ internal class AvatarTestFragment : Fragment() {
layoutParams.setMargins(margin, margin, margin, margin) layoutParams.setMargins(margin, margin, margin, margin)
imageView.layoutParams = layoutParams imageView.layoutParams = layoutParams
list.addView(imageView) list1.addView(imageView)
}
fun addBitmap(bitmap: Bitmap, width: Int, list: Int, targetContext: Context) {
val margin = padding
val imageView = ImageView(targetContext)
imageView.setImageBitmap(bitmap)
val layoutParams: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams(width, width)
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
layoutParams.setMargins(margin, margin, margin, margin)
imageView.layoutParams = layoutParams
if (list == 1) {
list1.addView(imageView)
} else {
list2.addView(imageView)
}
} }
companion object { companion object {

View file

@ -21,11 +21,9 @@
*/ */
package com.owncloud.android.ui.fragment package com.owncloud.android.ui.fragment
import android.Manifest
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.GrantPermissionRule
import com.nextcloud.client.TestActivity import com.nextcloud.client.TestActivity
import com.owncloud.android.AbstractIT import com.owncloud.android.AbstractIT
import com.owncloud.android.R import com.owncloud.android.R
@ -51,9 +49,6 @@ class FileDetailSharingFragmentIT : AbstractIT() {
@get:Rule @get:Rule
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
lateinit var file: OCFile lateinit var file: OCFile
lateinit var folder: OCFile lateinit var folder: OCFile
lateinit var activity: TestActivity lateinit var activity: TestActivity

View file

@ -18,10 +18,24 @@
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/avatar_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="horizontal">
<LinearLayout
android:id="@+id/avatar_list1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/avatar_list2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" />
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,24 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU 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 <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.account
internal class Status(val status: Enum<StatusType>, val message: String, val icon: String, val clearAt: String)

View file

@ -0,0 +1,27 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU 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 <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.account
enum class StatusType {
online, offline, dnd, away, unknown
}

View file

@ -27,25 +27,38 @@ import android.content.Context;
import android.content.UriMatcher; import android.content.UriMatcher;
import android.database.Cursor; import android.database.Cursor;
import android.database.MatrixCursor; import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns; import android.provider.BaseColumns;
import android.text.TextUtils;
import android.widget.Toast; import android.widget.Toast;
import com.nextcloud.client.account.Status;
import com.nextcloud.client.account.StatusType;
import com.nextcloud.client.account.User; import com.nextcloud.client.account.User;
import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManager;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation; import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation;
import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.ui.TextDrawable;
import com.owncloud.android.utils.BitmapUtils;
import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.ErrorMessageAdapter;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -72,6 +85,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
private static final String[] COLUMNS = { private static final String[] COLUMNS = {
BaseColumns._ID, BaseColumns._ID,
SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_ICON_1,
SearchManager.SUGGEST_COLUMN_INTENT_DATA SearchManager.SUGGEST_COLUMN_INTENT_DATA
}; };
@ -229,7 +243,8 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
Iterator<JSONObject> namesIt = names.iterator(); Iterator<JSONObject> namesIt = names.iterator();
JSONObject item; JSONObject item;
String displayName; String displayName;
int icon = 0; String subline = null;
Object icon = 0;
Uri dataUri; Uri dataUri;
int count = 0; int count = 0;
while (namesIt.hasNext()) { while (namesIt.hasNext()) {
@ -237,13 +252,26 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
dataUri = null; dataUri = null;
displayName = null; displayName = null;
String userName = item.getString(GetShareesRemoteOperation.PROPERTY_LABEL); String userName = item.getString(GetShareesRemoteOperation.PROPERTY_LABEL);
String name = item.isNull("name") ? "" : item.getString("name");
JSONObject value = item.getJSONObject(GetShareesRemoteOperation.NODE_VALUE); JSONObject value = item.getJSONObject(GetShareesRemoteOperation.NODE_VALUE);
ShareType type = ShareType.fromValue(value.getInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE)); ShareType type = ShareType.fromValue(value.getInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE));
String shareWith = value.getString(GetShareesRemoteOperation.PROPERTY_SHARE_WITH); String shareWith = value.getString(GetShareesRemoteOperation.PROPERTY_SHARE_WITH);
Status status;
JSONObject statusObject = item.optJSONObject("status");
if (statusObject != null) {
status = new Status(StatusType.valueOf(statusObject.getString("status")),
statusObject.isNull("message") ? "" : statusObject.getString("message"),
statusObject.isNull("icon") ? "" : statusObject.getString("icon"),
statusObject.isNull("clearAt") ? "" : statusObject.getString("clearAt"));
} else {
status = new Status(StatusType.unknown, "", "", "");
}
switch (type) { switch (type) {
case GROUP: case GROUP:
displayName = getContext().getString(R.string.share_group_clarification, userName); displayName = userName;
icon = R.drawable.ic_group; icon = R.drawable.ic_group;
dataUri = Uri.withAppendedPath(groupBaseUri, shareWith); dataUri = Uri.withAppendedPath(groupBaseUri, shareWith);
break; break;
@ -254,30 +282,47 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith); dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith);
if (userName.equals(shareWith)) { if (userName.equals(shareWith)) {
displayName = getContext().getString(R.string.share_remote_clarification, userName); displayName = name;
subline = getContext().getString(R.string.remote);
} else { } else {
String[] uriSplitted = shareWith.split("@"); String[] uriSplitted = shareWith.split("@");
displayName = getContext().getString(R.string.share_known_remote_clarification, displayName = name;
userName, uriSplitted[uriSplitted.length - 1]); subline = getContext().getString(R.string.share_known_remote_on_clarification,
uriSplitted[uriSplitted.length - 1]);
} }
} }
break; break;
case USER: case USER:
displayName = userName; displayName = userName;
icon = R.drawable.ic_user; subline = status.getMessage().isEmpty() ? null : status.getMessage();
Uri.Builder builder =
Uri.parse("content://com.nextcloud.android.providers.UsersAndGroupsSearchProvider/icon")
.buildUpon();
builder.appendQueryParameter("shareWith", shareWith);
builder.appendQueryParameter("displayName", displayName);
builder.appendQueryParameter("status", status.getStatus().toString());
if (!TextUtils.isEmpty(status.getIcon()) && !"null".equals(status.getIcon())) {
builder.appendQueryParameter("icon", status.getIcon());
}
icon = builder.build();
dataUri = Uri.withAppendedPath(userBaseUri, shareWith); dataUri = Uri.withAppendedPath(userBaseUri, shareWith);
break; break;
case EMAIL: case EMAIL:
icon = R.drawable.ic_email; icon = R.drawable.ic_email;
displayName = getContext().getString(R.string.share_email_clarification, userName); displayName = name;
subline = shareWith;
dataUri = Uri.withAppendedPath(emailBaseUri, shareWith); dataUri = Uri.withAppendedPath(emailBaseUri, shareWith);
break; break;
case ROOM: case ROOM:
icon = R.drawable.ic_chat_bubble; icon = R.drawable.ic_talk;
displayName = getContext().getString(R.string.share_room_clarification, userName); displayName = userName;
dataUri = Uri.withAppendedPath(roomBaseUri, shareWith); dataUri = Uri.withAppendedPath(roomBaseUri, shareWith);
break; break;
@ -295,6 +340,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
response.newRow() response.newRow()
.add(count++) // BaseColumns._ID .add(count++) // BaseColumns._ID
.add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_1 .add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_1
.add(subline) // SearchManager.SUGGEST_COLUMN_TEXT_2
.add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1 .add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1
.add(dataUri); .add(dataUri);
} }
@ -324,6 +370,56 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
return 0; return 0;
} }
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
String userId = uri.getQueryParameter("shareWith");
String displayName = uri.getQueryParameter("displayName");
String accountName = accountManager.getUser().getAccountName();
String serverName = accountName.substring(accountName.lastIndexOf('@') + 1);
String eTag = arbitraryDataProvider.getValue(userId + "@" + serverName, ThumbnailsCacheManager.AVATAR);
String avatarKey = "a_" + userId + "_" + serverName + "_" + eTag;
StatusType status = StatusType.valueOf(uri.getQueryParameter("status"));
String icon = uri.getQueryParameter("icon");
Bitmap avatarBitmap = ThumbnailsCacheManager.getBitmapFromDiskCache(avatarKey);
if (avatarBitmap == null) {
float avatarRadius = getContext().getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
avatarBitmap = BitmapUtils.drawableToBitmap(TextDrawable.createNamedAvatar(displayName, avatarRadius));
}
Bitmap avatar = BitmapUtils.createAvatarWithStatus(avatarBitmap, status, icon, getContext());
// create a file to write bitmap data
File f = new File(getContext().getCacheDir(), "test");
try {
f.createNewFile();
//Convert bitmap to byte array
ByteArrayOutputStream bos = new ByteArrayOutputStream();
avatar.compress(Bitmap.CompressFormat.PNG, 90, bos);
byte[] bitmapData = bos.toByteArray();
//write the bytes in file
try (FileOutputStream fos = new FileOutputStream(f)) {
fos.write(bitmapData);
} catch (FileNotFoundException e) {
Log_OC.e(TAG, "File not found: " + e.getMessage());
}
} catch (Exception e) {
Log_OC.e(TAG, "Error opening file: " + e.getMessage());
}
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}
/** /**
* Show error message * Show error message
* *

View file

@ -0,0 +1,127 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import com.owncloud.android.utils.BitmapUtils;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat;
/**
* A Drawable object that draws a status
*/
public class StatusDrawable extends Drawable {
private String mText = null;
private @DrawableRes int icon = -1;
private Paint mTextPaint;
private final Paint mBackground;
private final float radius;
private Context context;
public StatusDrawable(@DrawableRes int icon, float size, Context context) {
radius = size;
this.icon = icon;
this.context = context;
mBackground = new Paint();
mBackground.setStyle(Paint.Style.FILL);
mBackground.setAntiAlias(true);
mBackground.setColor(Color.argb(200, 255, 255, 255));
}
public StatusDrawable(BitmapUtils.Color color, float size) {
radius = size;
mBackground = new Paint();
mBackground.setStyle(Paint.Style.FILL);
mBackground.setAntiAlias(true);
mBackground.setColor(Color.argb(color.a, color.r, color.g, color.b));
}
public StatusDrawable(String icon, float size) {
mText = icon;
radius = size;
mBackground = new Paint();
mBackground.setStyle(Paint.Style.FILL);
mBackground.setAntiAlias(true);
mBackground.setColor(Color.argb(200, 255, 255, 255));
mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(size);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
/**
* Draw in its bounds (set via setBounds) respecting optional effects such as alpha (set via setAlpha) and color
* filter (set via setColorFilter) a circular background with a user's first character.
*
* @param canvas The canvas to draw into
*/
@Override
public void draw(@NonNull Canvas canvas) {
if (mBackground != null) {
canvas.drawCircle(radius, radius, radius, mBackground);
}
if (mText != null) {
mTextPaint.setTextSize(1.6f * radius);
canvas.drawText(mText, radius, radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
}
if (icon != -1) {
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), icon, null);
drawable.setBounds(0,
0,
(int) (2 * radius),
(int) (2 * radius));
drawable.draw(canvas);
}
}
@Override
public void setAlpha(int alpha) {
mTextPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mTextPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}

View file

@ -2,7 +2,7 @@
* ownCloud Android client application * ownCloud Android client application
* *
* @author Andy Scherzinger * @author Andy Scherzinger
* @author Tobias Kaminsiky * @author Tobias Kaminsky
* @author Chris Narkiewicz * @author Chris Narkiewicz
* Copyright (C) 2016 ownCloud Inc. * Copyright (C) 2016 ownCloud Inc.
* Copyright (C) 2018 Andy Scherzinger * Copyright (C) 2018 Andy Scherzinger
@ -35,7 +35,6 @@ import com.nextcloud.client.account.UserAccountManager;
import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.BitmapUtils;
import com.owncloud.android.utils.NextcloudServer; import com.owncloud.android.utils.NextcloudServer;
import java.security.NoSuchAlgorithmException;
import java.util.Locale; import java.util.Locale;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -65,6 +64,8 @@ public class TextDrawable extends Drawable {
*/ */
private float mRadius; private float mRadius;
private boolean bigText = false;
/** /**
* Create a TextDrawable with the given radius. * Create a TextDrawable with the given radius.
* *
@ -79,44 +80,43 @@ public class TextDrawable extends Drawable {
mBackground = new Paint(); mBackground = new Paint();
mBackground.setStyle(Paint.Style.FILL); mBackground.setStyle(Paint.Style.FILL);
mBackground.setAntiAlias(true); mBackground.setAntiAlias(true);
mBackground.setColor(Color.rgb(color.r, color.g, color.b)); mBackground.setColor(Color.argb(color.a, color.r, color.g, color.b));
mTextPaint = new Paint(); mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE); mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(radius); mTextPaint.setTextSize(radius);
mTextPaint.setAntiAlias(true); mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER); mTextPaint.setTextAlign(Paint.Align.CENTER);
setBounds(0, 0, (int) radius * 2, (int) radius * 2);
} }
/** /**
* creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the * creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the given
* given radius. * radius.
* *
* @param account user account * @param account user account
* @param radiusInDp the circle's radius * @param radiusInDp the circle's radius
* @return the avatar as a TextDrawable * @return the avatar as a TextDrawable
* @throws NoSuchAlgorithmException if the specified algorithm is not available when calculating the color values
*/ */
@NonNull @NonNull
@NextcloudServer(max = 12) @NextcloudServer(max = 12)
public static TextDrawable createAvatar(Account account, float radiusInDp) throws public static TextDrawable createAvatar(Account account, float radiusInDp) {
NoSuchAlgorithmException {
String username = UserAccountManager.getDisplayName(account); String username = UserAccountManager.getDisplayName(account);
return createNamedAvatar(username, radiusInDp); 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 * creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the given
* given radius. * radius.
* *
* @param userId userId to use * @param userId userId to use
* @param radiusInDp the circle's radius * @param radiusInDp the circle's radius
* @return the avatar as a TextDrawable * @return the avatar as a TextDrawable
* @throws NoSuchAlgorithmException if the specified algorithm is not available when calculating the color values
*/ */
@NonNull @NonNull
@NextcloudServer(max = 12) @NextcloudServer(max = 12)
public static TextDrawable createAvatarByUserId(String userId, float radiusInDp) throws NoSuchAlgorithmException { public static TextDrawable createAvatarByUserId(String userId, float radiusInDp) {
return createNamedAvatar(userId, radiusInDp); return createNamedAvatar(userId, radiusInDp);
} }
@ -127,10 +127,9 @@ public class TextDrawable extends Drawable {
* @param name the name * @param name the name
* @param radiusInDp the circle's radius * @param radiusInDp the circle's radius
* @return the avatar as a TextDrawable * @return the avatar as a TextDrawable
* @throws NoSuchAlgorithmException if the specified algorithm is not available when calculating the color values
*/ */
@NonNull @NonNull
public static TextDrawable createNamedAvatar(String name, float radiusInDp) throws NoSuchAlgorithmException { public static TextDrawable createNamedAvatar(String name, float radiusInDp) {
BitmapUtils.Color color = BitmapUtils.usernameToColor(name); BitmapUtils.Color color = BitmapUtils.usernameToColor(name);
return new TextDrawable(extractCharsFromDisplayName(name), color, radiusInDp); return new TextDrawable(extractCharsFromDisplayName(name), color, radiusInDp);
} }
@ -160,6 +159,11 @@ public class TextDrawable extends Drawable {
@Override @Override
public void draw(@NonNull Canvas canvas) { public void draw(@NonNull Canvas canvas) {
canvas.drawCircle(mRadius, mRadius, mRadius, mBackground); canvas.drawCircle(mRadius, mRadius, mRadius, mBackground);
if (bigText) {
mTextPaint.setTextSize(1.8f * mRadius);
}
canvas.drawText(mText, mRadius, mRadius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint); canvas.drawText(mText, mRadius, mRadius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
} }

View file

@ -31,8 +31,6 @@ import com.owncloud.android.databinding.FileDetailsShareShareItemBinding;
import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.ui.TextDrawable; import com.owncloud.android.ui.TextDrawable;
import java.security.NoSuchAlgorithmException;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -95,7 +93,7 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
private void setImage(ImageView avatar, String name, @DrawableRes int fallback) { private void setImage(ImageView avatar, String name, @DrawableRes int fallback) {
try { try {
avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension)); avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension));
} catch (NoSuchAlgorithmException | StringIndexOutOfBoundsException e) { } catch (StringIndexOutOfBoundsException e) {
avatar.setImageResource(fallback); avatar.setImageResource(fallback);
} }
} }

View file

@ -0,0 +1,55 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.components
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.RoundedBitmapDrawable
class AvatarWithStatus(val roundedBitmapDrawable: RoundedBitmapDrawable) : Drawable() {
private val redPaint: Paint = Paint().apply { setARGB(255, 255, 0, 0) }
override fun draw(canvas: Canvas) {
val width: Int = 100
val height: Int = 100
val radius: Float = Math.min(width, height).toFloat() / 2f
// Draw a red circle in the center
//canvas.drawBitmap(roundedBitmapDrawable.bitmap!!, 0f, 0f, null)
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, redPaint)
}
override fun setAlpha(alpha: Int) {
TODO("Not yet implemented")
}
override fun setColorFilter(colorFilter: ColorFilter?) {
TODO("Not yet implemented")
}
override fun getOpacity(): Int = PixelFormat.OPAQUE
}

View file

@ -76,6 +76,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.PopupMenu; import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@ -713,4 +714,10 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
private boolean canReshare(OCShare share) { private boolean canReshare(OCShare share) {
return (share.getPermissions() & SHARE_PERMISSION_FLAG) > 0; return (share.getPermissions() & SHARE_PERMISSION_FLAG) > 0;
} }
@VisibleForTesting
public void search(String query) {
SearchView searchView = getView().findViewById(R.id.searchView);
searchView.setQuery(query, true);
}
} }

View file

@ -18,19 +18,27 @@
*/ */
package com.owncloud.android.utils; package com.owncloud.android.utils;
import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options; import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.widget.ImageView; import android.widget.ImageView;
import com.nextcloud.client.account.StatusType;
import com.owncloud.android.MainApp; import com.owncloud.android.MainApp;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.StatusDrawable;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
@ -194,12 +202,19 @@ public final class BitmapUtils {
return resultBitmap; return resultBitmap;
} }
public static Color usernameToColor(String name) throws NoSuchAlgorithmException { public static Color usernameToColor(String name) {
String hash = name.toLowerCase(Locale.ROOT); String hash = name.toLowerCase(Locale.ROOT);
// already a md5 hash? // already a md5 hash?
if (!hash.matches("([0-9a-f]{4}-?){8}$")) { if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
hash = md5(hash); try {
hash = md5(hash);
} catch (NoSuchAlgorithmException e) {
int color = getResources().getColor(R.color.primary_dark);
return new Color(android.graphics.Color.red(color),
android.graphics.Color.green(color),
android.graphics.Color.blue(color));
}
} }
hash = hash.replaceAll("[^0-9a-f]", ""); hash = hash.replaceAll("[^0-9a-f]", "");
@ -279,6 +294,7 @@ public final class BitmapUtils {
} }
public static class Color { public static class Color {
public int a = 255;
public int r; public int r;
public int g; public int g;
public int b; public int b;
@ -289,6 +305,13 @@ public final class BitmapUtils {
this.b = b; this.b = b;
} }
public Color(int a, int r, int g, int b) {
this.a = a;
this.r = r;
this.g = g;
this.b = b;
}
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Color)) { if (!(obj instanceof Color)) {
@ -358,9 +381,16 @@ public final class BitmapUtils {
Bitmap bitmap; Bitmap bitmap;
if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); if (drawable.getBounds().width() > 0 && drawable.getBounds().height() > 0) {
bitmap = Bitmap.createBitmap(drawable.getBounds().width(),
drawable.getBounds().height(),
Bitmap.Config.ARGB_8888);
} else {
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
} else { } else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888); Bitmap.Config.ARGB_8888);
} }
@ -384,6 +414,90 @@ public final class BitmapUtils {
imageView); imageView);
} }
public static Bitmap createAvatarWithStatus(Bitmap avatar, StatusType status, String icon, Context context) {
float avatarRadius = getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
int width = DisplayUtils.convertDpToPixel(2 * avatarRadius, context);
Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
// avatar
Bitmap croppedBitmap = getCroppedBitmap(avatar, width);
canvas.drawBitmap(croppedBitmap, 0f, 0f, null);
// status
int statusSize = width / 4;
StatusDrawable statusDrawable;
if (TextUtils.isEmpty(icon)) {
switch (status) {
case dnd:
statusDrawable = new StatusDrawable(R.drawable.ic_user_status_dnd, statusSize, context);
statusDrawable.setBounds(width / 2,
width / 2,
width,
width);
break;
case online:
statusDrawable = new StatusDrawable(new Color(255, 73, 179, 130), statusSize);
statusDrawable.setBounds(width,
width,
width,
width);
break;
case away:
statusDrawable = new StatusDrawable(R.drawable.ic_user_status_away, statusSize, context);
statusDrawable.setBounds(width / 2,
width / 2,
width,
width);
break;
default:
// do not show
statusDrawable = null;
break;
}
} else {
statusDrawable = new StatusDrawable(icon, statusSize);
statusDrawable.setBounds(width / 2,
width / 2,
width,
width);
}
if (statusDrawable != null) {
canvas.translate(width / 2f, width / 2f);
statusDrawable.draw(canvas);
}
return output;
}
/**
* from https://stackoverflow.com/a/12089127
*/
private static Bitmap getCroppedBitmap(Bitmap bitmap, int width) {
Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
int color = -0xbdbdbe;
Paint paint = new Paint();
Rect rect = new Rect(0, 0, width, width);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawCircle(width / 2f, width / 2f, width / 2f, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(Bitmap.createScaledBitmap(bitmap, width, width, false), rect, rect, paint);
return output;
}
private static Resources getResources() { private static Resources getResources() {
return MainApp.getAppContext().getResources(); return MainApp.getAppContext().getResources();
} }

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:fillColor="#757575"
android:pathData="M63.992,0.689C29.031,0.689 0.691,29.031 0.692,63.992c0,34.96 28.34,63.301 63.3,63.302 6.982,-0.014 13.881,-1.183 20.426,-3.43 4.317,-1.482 8.48,-3.433 12.411,-5.831 3.383,1.344 8.59,3.838 13.736,5.902 6.688,2.683 13.274,4.639 15.618,2.399 2.317,-2.212 0.703,-8.809 -1.647,-15.575 -2.046,-5.892 -4.649,-11.913 -5.701,-15.282 2.544,-4.415 4.535,-9.101 5.945,-13.954 1.648,-5.674 2.5,-11.574 2.512,-17.532C127.291,29.032 98.952,0.692 63.992,0.691ZM63.999,24.756l0.001,0c21.677,0 39.25,17.573 39.25,39.251 -0.001,21.677 -17.574,39.249 -39.251,39.249 -21.676,0 -39.249,-17.572 -39.25,-39.249 0,-21.678 17.573,-39.251 39.25,-39.251z"
android:strokeWidth="4.78543139" />
</vector>

View file

@ -0,0 +1,32 @@
<!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2020 Tobias Kaminsky
~ Copyright (C) 2020 Nextcloud GmbH
~
~ 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
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU 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 <https://www.gnu.org/licenses/>.
-->
<vector android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#f4a331"
android:pathData="m10.615,2.1094c-4.8491,0.6811 -8.6152,4.8615 -8.6152,9.8906 0,5.5 4.5,10 10,10 5.0292,0 9.2096,-3.7661 9.8906,-8.6152 -1.4654,1.601 -3.5625,2.6152 -5.8906,2.6152 -4.4,0 -8,-3.6 -8,-8 0,-2.3281 1.0143,-4.4252 2.6152,-5.8906z" />
</vector>

View file

@ -0,0 +1,38 @@
<!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2020 Tobias Kaminsky
~ Copyright (C) 2020 Nextcloud GmbH
~
~ 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
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU 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 <https://www.gnu.org/licenses/>.
-->
<vector android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#ed484c"
android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10z" />
<path
android:fillColor="#fdffff"
android:pathData="m8,10h8c1.108,0 2,0.892 2,2s-0.892,2 -2,2h-8c-1.108,0 -2,-0.892 -2,-2s0.892,-2 2,-2z"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="2" />
</vector>

View file

@ -479,7 +479,7 @@
<string name="share_remote_clarification">%1$s (remote)</string> <string name="share_remote_clarification">%1$s (remote)</string>
<string name="share_email_clarification">%1$s (email)</string> <string name="share_email_clarification">%1$s (email)</string>
<string name="share_room_clarification">%1$s (conversation)</string> <string name="share_room_clarification">%1$s (conversation)</string>
<string name="share_known_remote_clarification">%1$s ( at %2$s )</string> <string name="share_known_remote_on_clarification">on %1$s</string>
<string name="share_privilege_unshare">Unshare</string> <string name="share_privilege_unshare">Unshare</string>
@ -936,4 +936,5 @@
<string name="link_share_file_drop">File drop (upload only)</string> <string name="link_share_file_drop">File drop (upload only)</string>
<string name="could_not_retrieve_shares">Could not retrieve shares</string> <string name="could_not_retrieve_shares">Could not retrieve shares</string>
<string name="failed_update_ui">Failed to update UI</string> <string name="failed_update_ui">Failed to update UI</string>
<string name="remote">(remote)</string>
</resources> </resources>