VirusTotal: list virus scanner results

This commit is contained in:
M66B 2022-07-25 10:42:42 +02:00
parent 8656c9c59b
commit e0c3e50fdb
8 changed files with 410 additions and 108 deletions

20
FAQ.md
View File

@ -4858,21 +4858,15 @@ else you can double tap or long press the marked text to show suggestions.
<a name="faq181"></a>
**(181) How do I use VirusTotal?**
[VirusTotal](https://www.virustotal.com/) integration needs to be enabled in the miscellaneous settings.
This will show a *scan* icon button for each attachment.
VirusTotal integration needs to be enabled in the miscellaneous settings and an API key needs to be entered.
To get an API key, you'll need to sign up via the [VirusTotal website](https://www.virustotal.com/).
Without entering an API key, tapping on the scan button will calculate the SHA-256 hash of the attached file and open the corresponding file report on the VirusTotal website.
If the file is not known by VirusTotal ("*Item not found*"), it is probably okay, unless it contains a new virus not being detected by virus scanners yet.
When integration is enabled and an API key is available, a *scan* icon button will be shown for each attachment.
Tapping on the scan button will calculate the SHA-256 hash of the attachment and lookup the file via the VirusTotal API.
If the file is known by VirusTotal, the number of virus scanners considering the file as malicious will be shown.
If the file isn't known by VirusTotal, an upload button will be shown to upload the file for analysis by VirusTotal.
With entering an API key, there will be a dialog showing the number of virus scanners detecting the file.
Tapping on the info button will open the corresponding file report on the VirusTotal website.
To get an API key, you'll need to register on the VirusTotal website.
You can enter the API key in the miscellaneous settings of the app.
Note that only the hash of a file will be sent and that files won't be uploaded.
This feature was added in version 1.1941 and is available in non Play store versions only.
This feature was added in version 1.1942 and is available in non Play store versions of the app only.
<br />

View File

@ -21,8 +21,12 @@ package eu.faircode.email;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -37,6 +41,8 @@ import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import javax.net.ssl.HttpsURLConnection;
@ -72,25 +78,17 @@ public class VirusTotal {
JSONObject jclassification = jattributes.optJSONObject("popular_threat_classification");
String label = (jclassification == null ? null : jclassification.getString("suggested_threat_label"));
int count = 0;
int malicious = 0;
List<ScanResult> scanResult = new ArrayList<>();
JSONObject janalysis = jattributes.getJSONObject("last_analysis_results");
JSONArray jnames = janalysis.names();
for (int i = 0; i < jnames.length(); i++) {
String name = jnames.getString(i);
JSONObject jresult = janalysis.getJSONObject(name);
String category = jresult.getString("category");
//Log.i("VT " + name + "=" + category);
if (!"type-unsupported".equals(category))
count++;
if ("malicious".equals(category))
malicious++;
scanResult.add(new ScanResult(name, category));
}
Log.i("VT lookup=" + malicious + "/" + count + " label=" + label);
result.putInt("count", count);
result.putInt("malicious", malicious);
result.putParcelableArrayList("scans", (ArrayList<? extends Parcelable>) scanResult);
result.putString("label", label);
}
@ -236,4 +234,42 @@ public class VirusTotal {
connection.disconnect();
}
}
public static class ScanResult implements Parcelable {
public String name;
public String category;
ScanResult(String name, String category) {
this.name = name;
this.category = category;
}
protected ScanResult(Parcel in) {
name = in.readString();
category = in.readString();
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
parcel.writeString(name);
parcel.writeString(category);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<ScanResult> CREATOR = new Creator<ScanResult>() {
@Override
public ScanResult createFromParcel(Parcel in) {
return new ScanResult(in);
}
@Override
public ScanResult[] newArray(int size) {
return new ScanResult[size];
}
};
}
}

View File

@ -21,10 +21,10 @@ package eu.faircode.email;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
@ -41,7 +41,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
@ -51,10 +50,12 @@ import androidx.lifecycle.OnLifecycleEvent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import java.io.File;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -71,6 +72,7 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
private boolean readonly;
private boolean vt_enabled;
private String vt_apikey;
private boolean debug;
private int dp12;
private int dp36;
@ -114,8 +116,7 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
view.setOnClickListener(this);
ibDelete.setOnClickListener(this);
ibSave.setOnClickListener(this);
if (vt_enabled)
ibScan.setOnClickListener(this);
ibScan.setOnClickListener(this);
view.setOnLongClickListener(this);
}
@ -123,8 +124,7 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
view.setOnClickListener(null);
ibDelete.setOnClickListener(null);
ibSave.setOnClickListener(null);
if (vt_enabled)
ibScan.setOnClickListener(null);
ibScan.setOnClickListener(null);
view.setOnLongClickListener(null);
}
@ -183,7 +183,9 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
}
ibSave.setVisibility(attachment.available ? View.VISIBLE : View.GONE);
ibScan.setVisibility(attachment.available && vt_enabled ? View.VISIBLE : View.GONE);
ibScan.setVisibility(attachment.available &&
vt_enabled && !TextUtils.isEmpty(vt_apikey) && !BuildConfig.PLAY_STORE_RELEASE
? View.VISIBLE : View.GONE);
if (attachment.progress != null)
progressbar.setProgress(attachment.progress);
@ -319,11 +321,8 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
}
private void onScan(EntityAttachment attachment) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String apiKey = prefs.getString("vt_apikey", null);
Bundle args = new Bundle();
args.putString("apiKey", apiKey);
args.putString("apiKey", vt_apikey);
args.putString("name", attachment.name);
args.putSerializable("file", attachment.getFile(context));
@ -402,7 +401,8 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
this.inflater = LayoutInflater.from(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.vt_enabled = (prefs.getBoolean("vt_enabled", false) && !BuildConfig.PLAY_STORE_RELEASE);
this.vt_enabled = prefs.getBoolean("vt_enabled", false);
this.vt_apikey = prefs.getString("vt_apikey", null);
this.debug = prefs.getBoolean("debug", false);
this.dp12 = Helper.dp2pixels(context, 12);
this.dp36 = Helper.dp2pixels(context, 36);
@ -534,27 +534,44 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
final Context context = getContext();
View view = LayoutInflater.from(context).inflate(R.layout.dialog_virus_total, null);
final TextView tvName = view.findViewById(R.id.tvName);
final ProgressBar pbAnalysis = view.findViewById(R.id.pbAnalysis);
final TextView tvCount = view.findViewById(R.id.tvCount);
final TextView tvLabel = view.findViewById(R.id.tvLabel);
final TextView tvError = view.findViewById(R.id.tvError);
final TextView tvUnknown = view.findViewById(R.id.tvUnknown);
final TextView tvSummary = view.findViewById(R.id.tvSummary);
final TextView tvLabel = view.findViewById(R.id.tvLabel);
final TextView tvReport = view.findViewById(R.id.tvReport);
final RecyclerView rvScan = view.findViewById(R.id.rvScan);
final Button btnUpload = view.findViewById(R.id.btnUpload);
final ProgressBar pbUpload = view.findViewById(R.id.pbUpload);
final ProgressBar pbWait = view.findViewById(R.id.pbWait);
final TextView tvAnalyzing = view.findViewById(R.id.tvAnalyzing);
final Group grpAnalysis = view.findViewById(R.id.grpAnalysis);
final ProgressBar pbWait = view.findViewById(R.id.pbWait);
tvName.setText(name);
tvLabel.setVisibility(View.GONE);
tvName.setVisibility(TextUtils.isEmpty(name) ? View.GONE : View.VISIBLE);
tvError.setVisibility(View.GONE);
tvUnknown.setVisibility(View.GONE);
tvSummary.setVisibility(View.GONE);
tvLabel.setVisibility(View.GONE);
tvReport.getPaint().setUnderlineText(true);
tvReport.setVisibility(View.GONE);
rvScan.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvScan.setLayoutManager(llm);
final AdapterVirusTotal adapter = new AdapterVirusTotal(getContext(), getViewLifecycleOwner());
rvScan.setAdapter(adapter);
rvScan.setVisibility(View.GONE);
btnUpload.setVisibility(View.GONE);
pbUpload.setVisibility(View.GONE);
tvAnalyzing.setVisibility(View.GONE);
grpAnalysis.setVisibility(View.GONE);
pbWait.setVisibility(View.GONE);
final SimpleTask<Bundle> taskLookup = new SimpleTask<Bundle>() {
@Override
protected void onPreExecute(Bundle args) {
tvError.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
}
@ -572,23 +589,34 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
@Override
protected void onExecuted(Bundle args, Bundle result) {
int count = result.getInt("count", -1);
int malicious = result.getInt("malicious", -1);
List<VirusTotal.ScanResult> scans = result.getParcelableArrayList("scans");
String label = result.getString("label");
pbAnalysis.setMax(count);
pbAnalysis.setProgress(malicious);
tvCount.setText(malicious + "/" + count);
int malicious = 0;
if (scans != null)
for (VirusTotal.ScanResult scan : scans)
if ("malicious".equals(scan.category))
malicious++;
NumberFormat NF = NumberFormat.getNumberInstance();
tvUnknown.setVisibility(scans == null ? View.VISIBLE : View.GONE);
tvSummary.setText(getString(R.string.title_vt_summary, NF.format(malicious)));
tvSummary.setTextColor(Helper.resolveColor(context,
malicious == 0 ? android.R.attr.textColorPrimary : R.attr.colorWarning));
tvSummary.setTypeface(malicious == 0 ? Typeface.DEFAULT : Typeface.DEFAULT_BOLD);
tvSummary.setVisibility(scans == null ? View.GONE : View.VISIBLE);
tvLabel.setText(label);
tvLabel.setVisibility(TextUtils.isEmpty(label) ? View.GONE : View.VISIBLE);
tvUnknown.setVisibility(count == 0 ? View.VISIBLE : View.GONE);
btnUpload.setVisibility(count == 0 && !TextUtils.isEmpty(apiKey) ? View.VISIBLE : View.GONE);
grpAnalysis.setVisibility(count == 0 ? View.GONE : View.VISIBLE);
tvReport.setVisibility(scans == null ? View.GONE : View.VISIBLE);
adapter.set(scans == null ? new ArrayList<>() : scans);
rvScan.setVisibility(scans == null ? View.GONE : View.VISIBLE);
btnUpload.setVisibility(scans == null && !TextUtils.isEmpty(apiKey) ? View.VISIBLE : View.GONE);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
tvError.setText(Log.formatThrowable(ex, false));
tvError.setVisibility(View.VISIBLE);
}
};
@ -655,6 +683,13 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
}
};
tvReport.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
taskUrl.execute(FragmentDialogVirusTotal.this, args, "attachment:report");
}
});
btnUpload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@ -669,13 +704,7 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
return new AlertDialog.Builder(context)
.setView(view)
.setPositiveButton(R.string.title_info, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
taskUrl.execute(FragmentDialogVirusTotal.this, args, "attachment:virustotal");
}
})
.setNegativeButton(android.R.string.cancel, null)
.setNegativeButton(R.string.title_setup_done, null)
.create();
}
}

View File

@ -0,0 +1,191 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2022 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class AdapterVirusTotal extends RecyclerView.Adapter<AdapterVirusTotal.ViewHolder> {
private Context context;
private LifecycleOwner owner;
private LayoutInflater inflater;
private int colorWarning;
private int textColorSecondary;
private List<VirusTotal.ScanResult> items = new ArrayList<>();
public class ViewHolder extends RecyclerView.ViewHolder {
private View view;
private TextView tvName;
private TextView tvCategory;
ViewHolder(View itemView) {
super(itemView);
view = itemView.findViewById(R.id.clItem);
tvName = itemView.findViewById(R.id.tvName);
tvCategory = itemView.findViewById(R.id.tvCategory);
}
private void wire() {
}
private void unwire() {
}
private void bindTo(VirusTotal.ScanResult scan) {
boolean malicious = "malicious".equals(scan.category);
tvName.setText(scan.name);
tvCategory.setText(scan.category);
tvCategory.setTextColor(malicious ? colorWarning : textColorSecondary);
tvCategory.setTypeface(malicious ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
}
}
AdapterVirusTotal(Context context, LifecycleOwner owner) {
this.context = context;
this.owner = owner;
this.inflater = LayoutInflater.from(context);
this.colorWarning = Helper.resolveColor(context, R.attr.colorWarning);
this.textColorSecondary = Helper.resolveColor(context, android.R.attr.textColorSecondary);
setHasStableIds(true);
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
Log.d(AdapterVirusTotal.this + " parent destroyed");
owner.getLifecycle().removeObserver(this);
}
});
}
public void set(@NonNull List<VirusTotal.ScanResult> scans) {
Log.i("Set scans=" + scans.size());
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, scans), false);
items = scans;
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.d("Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.d("Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.d("Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.d("Changed @" + position + " #" + count);
}
});
try {
diff.dispatchUpdatesTo(this);
} catch (Throwable ex) {
Log.e(ex);
}
}
private static class DiffCallback extends DiffUtil.Callback {
private List<VirusTotal.ScanResult> prev = new ArrayList<>();
private List<VirusTotal.ScanResult> next = new ArrayList<>();
DiffCallback(List<VirusTotal.ScanResult> prev, List<VirusTotal.ScanResult> next) {
this.prev.addAll(prev);
this.next.addAll(next);
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
VirusTotal.ScanResult m1 = prev.get(oldItemPosition);
VirusTotal.ScanResult m2 = next.get(newItemPosition);
return m1.name.equals(m2.name);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
VirusTotal.ScanResult m1 = prev.get(oldItemPosition);
VirusTotal.ScanResult m2 = next.get(newItemPosition);
return (m1.name.equals(m2.name) &&
Objects.equals(m1.category, m2.category));
}
}
@Override
public long getItemId(int position) {
return items.get(position).name.hashCode();
}
@Override
public int getItemCount() {
return items.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.item_virus_total, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
VirusTotal.ScanResult scan = items.get(position);
holder.bindTo(scan);
holder.wire();
}
}

View File

@ -157,7 +157,8 @@ public class FragmentOptions extends FragmentBase {
"webview_legacy", "browser_zoom", "fake_dark",
"show_recent",
"biometrics",
"default_light"
"default_light",
"vt_enabled", "vt_apikey"
};
@Override

View File

@ -28,52 +28,23 @@
android:layout_marginTop="24dp"
android:text="File name"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<TextView
android:id="@+id/tvAnalysis"
android:id="@+id/tvError"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/title_vt_count"
android:layout_marginTop="12dp"
android:fontFamily="monospace"
android:text="Error"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?attr/colorWarning"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<ProgressBar
android:id="@+id/pbAnalysis"
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginEnd="6dp"
android:progress="50"
android:scaleY="3"
app:layout_constraintEnd_toStartOf="@+id/tvCount"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAnalysis" />
<TextView
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="10/100"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@id/pbAnalysis"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/tvLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Label"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pbAnalysis" />
<TextView
android:id="@+id/tvUnknown"
android:layout_width="wrap_content"
@ -82,7 +53,7 @@
android:text="@string/title_vt_unknown"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvLabel" />
app:layout_constraintTop_toBottomOf="@id/tvError" />
<Button
android:id="@+id/btnUpload"
@ -104,6 +75,7 @@
android:layout_marginStart="12dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/btnUpload"
app:layout_constraintBottom_toTopOf="@+id/tvAnalyzing"
app:layout_constraintStart_toEndOf="@id/btnUpload"
app:layout_constraintTop_toTopOf="@id/btnUpload" />
@ -117,6 +89,53 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnUpload" />
<TextView
android:id="@+id/tvSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_vt_summary"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?attr/colorWarning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAnalyzing" />
<TextView
android:id="@+id/tvLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Label"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSummary" />
<TextView
android:id="@+id/tvReport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:drawableEnd="@drawable/twotone_open_in_new_12"
android:drawablePadding="6dp"
android:drawableTint="?android:attr/textColorLink"
android:text="@string/title_vt_report"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorLink"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvLabel" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvScan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:nestedScrollingEnabled="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvReport" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
@ -127,11 +146,5 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpAnalysis"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvAnalysis,pbAnalysis,tvCount" />
</androidx.constraintlayout.widget.ConstraintLayout>
</eu.faircode.email.ScrollViewEx>
</eu.faircode.email.ScrollViewEx>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/clItem"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:maxLines="1"
android:text="Name"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toStartOf="@+id/tvCategory"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvCategory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:ellipsize="middle"
android:singleLine="true"
android:text="Category"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -1467,10 +1467,11 @@
<string name="title_send_dsn">Status report</string>
<string name="title_send_receipt">Request receipt</string>
<string name="title_send_receipt_remark">Most providers and email clients ignore receipt requests</string>
<string name="title_vt_count">Number of virus scanners that flag this file as malicious:</string>
<string name="title_vt_unknown">This file hasn\'t been seen by VirusTotal before, so it has not been scanned yet</string>
<string name="title_vt_unknown">This file is not yet known to VirusTotal</string>
<string name="title_vt_summary">%1$s virus scanners report this file as malicious</string>
<string name="title_vt_report">Full report</string>
<string name="title_vt_upload">Upload</string>
<string name="title_vt_analyzing">Analyzing, this may take a while &#8230;</string>
<string name="title_vt_analyzing">VirusTotal is analyzing the file, this may take a while &#8230;</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_pgp_reminder">PGP keys available</string>