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
scripts/runAllScreenshotCombinations "noCI" "$1" "-Pandroid.testInstrumentationRunnerArguments.class=$class$method"
else
./gradlew gplayDebugExecuteScreenshotTests $record \
./gradlew --offline gplayDebugExecuteScreenshotTests $record \
-Pandroid.testInstrumentationRunnerArguments.annotation=com.owncloud.android.utils.ScreenshotTest \
-Pandroid.testInstrumentationRunnerArguments.class=$class$method \
$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
import android.graphics.BitmapFactory
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.nextcloud.client.TestActivity
import com.nextcloud.client.account.StatusType
import com.owncloud.android.AbstractIT
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.ScreenshotTest
import org.junit.Rule
import org.junit.Test
class AvatarIT : AbstractIT() {
@get:Rule
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
@ -60,4 +65,124 @@ class AvatarIT : AbstractIT() {
waitForIdleSync()
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
import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -34,12 +35,14 @@ import com.owncloud.android.R
import com.owncloud.android.ui.TextDrawable
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? {
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
}
@ -54,7 +57,25 @@ internal class AvatarTestFragment : Fragment() {
layoutParams.setMargins(margin, margin, margin, margin)
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 {

View file

@ -21,11 +21,9 @@
*/
package com.owncloud.android.ui.fragment
import android.Manifest
import android.widget.ImageView
import androidx.appcompat.widget.PopupMenu
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.GrantPermissionRule
import com.nextcloud.client.TestActivity
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
@ -51,9 +49,6 @@ class FileDetailSharingFragmentIT : AbstractIT() {
@get:Rule
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 folder: OCFile
lateinit var activity: TestActivity

View file

@ -18,10 +18,24 @@
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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/avatar_list"
android:layout_width="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>

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.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.text.TextUtils;
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.UserAccountManager;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
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.utils.Log_OC;
import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation;
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 org.json.JSONException;
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.HashMap;
import java.util.Iterator;
@ -72,6 +85,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
private static final String[] COLUMNS = {
BaseColumns._ID,
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_ICON_1,
SearchManager.SUGGEST_COLUMN_INTENT_DATA
};
@ -229,7 +243,8 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
Iterator<JSONObject> namesIt = names.iterator();
JSONObject item;
String displayName;
int icon = 0;
String subline = null;
Object icon = 0;
Uri dataUri;
int count = 0;
while (namesIt.hasNext()) {
@ -237,13 +252,26 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
dataUri = null;
displayName = null;
String userName = item.getString(GetShareesRemoteOperation.PROPERTY_LABEL);
String name = item.isNull("name") ? "" : item.getString("name");
JSONObject value = item.getJSONObject(GetShareesRemoteOperation.NODE_VALUE);
ShareType type = ShareType.fromValue(value.getInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE));
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) {
case GROUP:
displayName = getContext().getString(R.string.share_group_clarification, userName);
displayName = userName;
icon = R.drawable.ic_group;
dataUri = Uri.withAppendedPath(groupBaseUri, shareWith);
break;
@ -254,30 +282,47 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith);
if (userName.equals(shareWith)) {
displayName = getContext().getString(R.string.share_remote_clarification, userName);
displayName = name;
subline = getContext().getString(R.string.remote);
} else {
String[] uriSplitted = shareWith.split("@");
displayName = getContext().getString(R.string.share_known_remote_clarification,
userName, uriSplitted[uriSplitted.length - 1]);
displayName = name;
subline = getContext().getString(R.string.share_known_remote_on_clarification,
uriSplitted[uriSplitted.length - 1]);
}
}
break;
case USER:
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);
break;
case EMAIL:
icon = R.drawable.ic_email;
displayName = getContext().getString(R.string.share_email_clarification, userName);
displayName = name;
subline = shareWith;
dataUri = Uri.withAppendedPath(emailBaseUri, shareWith);
break;
case ROOM:
icon = R.drawable.ic_chat_bubble;
displayName = getContext().getString(R.string.share_room_clarification, userName);
icon = R.drawable.ic_talk;
displayName = userName;
dataUri = Uri.withAppendedPath(roomBaseUri, shareWith);
break;
@ -295,6 +340,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
response.newRow()
.add(count++) // BaseColumns._ID
.add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_1
.add(subline) // SearchManager.SUGGEST_COLUMN_TEXT_2
.add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1
.add(dataUri);
}
@ -324,6 +370,56 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
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
*

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
*
* @author Andy Scherzinger
* @author Tobias Kaminsiky
* @author Tobias Kaminsky
* @author Chris Narkiewicz
* Copyright (C) 2016 ownCloud Inc.
* 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.NextcloudServer;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import androidx.annotation.NonNull;
@ -65,6 +64,8 @@ public class TextDrawable extends Drawable {
*/
private float mRadius;
private boolean bigText = false;
/**
* Create a TextDrawable with the given radius.
*
@ -79,44 +80,43 @@ public class TextDrawable extends Drawable {
mBackground = new Paint();
mBackground.setStyle(Paint.Style.FILL);
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.setColor(Color.WHITE);
mTextPaint.setTextSize(radius);
mTextPaint.setAntiAlias(true);
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
* given radius.
* creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the given
* radius.
*
* @param account user account
* @param radiusInDp the circle's radius
* @return the avatar as a TextDrawable
* @throws NoSuchAlgorithmException if the specified algorithm is not available when calculating the color values
*/
@NonNull
@NextcloudServer(max = 12)
public static TextDrawable createAvatar(Account account, float radiusInDp) throws
NoSuchAlgorithmException {
public static TextDrawable createAvatar(Account account, float radiusInDp) {
String username = UserAccountManager.getDisplayName(account);
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.
* 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 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 NoSuchAlgorithmException {
public static TextDrawable createAvatarByUserId(String userId, float radiusInDp) {
return createNamedAvatar(userId, radiusInDp);
}
@ -127,10 +127,9 @@ public class TextDrawable extends Drawable {
* @param name the name
* @param radiusInDp the circle's radius
* @return the avatar as a TextDrawable
* @throws NoSuchAlgorithmException if the specified algorithm is not available when calculating the color values
*/
@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);
return new TextDrawable(extractCharsFromDisplayName(name), color, radiusInDp);
}
@ -160,6 +159,11 @@ public class TextDrawable extends Drawable {
@Override
public void draw(@NonNull Canvas canvas) {
canvas.drawCircle(mRadius, mRadius, mRadius, mBackground);
if (bigText) {
mTextPaint.setTextSize(1.8f * mRadius);
}
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.ui.TextDrawable;
import java.security.NoSuchAlgorithmException;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@ -95,7 +93,7 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
private void setImage(ImageView avatar, String name, @DrawableRes int fallback) {
try {
avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension));
} catch (NoSuchAlgorithmException | StringIndexOutOfBoundsException e) {
} catch (StringIndexOutOfBoundsException e) {
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.VisibleForTesting;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -713,4 +714,10 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
private boolean canReshare(OCShare share) {
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;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
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.Drawable;
import android.text.TextUtils;
import android.widget.ImageView;
import com.nextcloud.client.account.StatusType;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.StatusDrawable;
import org.apache.commons.codec.binary.Hex;
@ -194,12 +202,19 @@ public final class BitmapUtils {
return resultBitmap;
}
public static Color usernameToColor(String name) throws NoSuchAlgorithmException {
public static Color usernameToColor(String name) {
String hash = name.toLowerCase(Locale.ROOT);
// already a md5 hash?
if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
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]", "");
@ -279,6 +294,7 @@ public final class BitmapUtils {
}
public static class Color {
public int a = 255;
public int r;
public int g;
public int b;
@ -289,6 +305,13 @@ public final class BitmapUtils {
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
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Color)) {
@ -358,9 +381,16 @@ public final class BitmapUtils {
Bitmap bitmap;
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(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888);
}
@ -384,6 +414,90 @@ public final class BitmapUtils {
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() {
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_email_clarification">%1$s (email)</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>
@ -936,4 +936,5 @@
<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="remote">(remote)</string>
</resources>