Upgrade clipboard and timeout

This commit is contained in:
J-Jamet
2018-03-18 13:26:13 +01:00
parent 42ac83c814
commit accb931831
12 changed files with 264 additions and 258 deletions

View File

@@ -21,22 +21,11 @@
package com.keepassdroid.activities;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v7.widget.Toolbar;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -50,11 +39,10 @@ import com.keepassdroid.compat.ActivityCompat;
import com.keepassdroid.database.Database;
import com.keepassdroid.database.PwDatabase;
import com.keepassdroid.database.PwEntry;
import com.keepassdroid.database.exception.SamsungClipboardException;
import com.keepassdroid.password.PasswordActivity;
import com.keepassdroid.services.NotificationCopyingService;
import com.keepassdroid.settings.PreferencesUtil;
import com.keepassdroid.tasks.UIToastTask;
import com.keepassdroid.timeout.ClipboardHelper;
import com.keepassdroid.utils.EmptyUtils;
import com.keepassdroid.utils.MenuUtil;
import com.keepassdroid.utils.Types;
@@ -64,8 +52,6 @@ import com.kunzisoft.keepass.R;
import java.util.Date;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import static com.keepassdroid.settings.PreferencesUtil.isClipboardNotificationsEnable;
@@ -73,19 +59,16 @@ import static com.keepassdroid.settings.PreferencesUtil.isClipboardNotifications
public class EntryActivity extends LockingHideActivity {
public static final String KEY_ENTRY = "entry";
public static final String COPY_USERNAME = "com.keepassdroid.copy_username";
public static final String COPY_PASSWORD = "com.keepassdroid.copy_password";
private ImageView titleIconView;
private TextView titleView;
private EntryContentsView entryContentsView;
protected PwEntry mEntry;
private Timer mTimer = new Timer();
private boolean mShowPassword;
private BroadcastReceiver mIntentReceiver;
protected boolean readOnly = false;
private ClipboardHelper clipboardHelper;
public static void launch(Activity act, PwEntry pw) {
Intent intent = new Intent(act, EntryActivity.class);
intent.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID()));
@@ -141,68 +124,36 @@ public class EntryActivity extends LockingHideActivity {
// Setup Edit Buttons
View edit = findViewById(R.id.entry_edit);
edit.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
EntryEditActivity.Launch(EntryActivity.this, mEntry);
}
});
edit.setOnClickListener(v -> EntryEditActivity.Launch(EntryActivity.this, mEntry));
if (readOnly) {
edit.setVisibility(View.GONE);
}
// If notifications enabled in settings
if (isClipboardNotificationsEnable(getApplicationContext())) {
if (mEntry.getPassword().length() > 0) {
// username already copied, waiting for user's action before copy password.
Intent intent = new Intent(this, NotificationCopyingService.class);
intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION);
intent.putExtra(NotificationCopyingService.EXTRA_PASSWORD, mEntry.getPassword());
if (mEntry.getUsername().length() > 0) {
intent.putExtra(NotificationCopyingService.EXTRA_USERNAME, mEntry.getUsername());
}
if (mEntry.getTitle() != null)
intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle());
startService(intent);
}
}
mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if ( action != null) {
if (action.equals(COPY_USERNAME)) {
String username = mEntry.getUsername();
if (username.length() > 0) {
timeoutCopyToClipboard(username);
}
} else if (action.equals(COPY_PASSWORD)) {
String password = mEntry.getPassword();
if (password.length() > 0) {
timeoutCopyToClipboard(password);
}
}
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(COPY_USERNAME);
filter.addAction(COPY_PASSWORD);
registerReceiver(mIntentReceiver, filter);
// Init the clipboard helper
clipboardHelper = new ClipboardHelper(this);
}
@Override
protected void onDestroy() {
// These members might never get initialized if the app timed out
if ( mIntentReceiver != null ) {
unregisterReceiver(mIntentReceiver);
}
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
// If notifications enabled in settings
// Don't if application timeout
if (!App.isShutdown() && isClipboardNotificationsEnable(getApplicationContext())) {
if (mEntry.getPassword().length() > 0) {
// username already copied, waiting for user's action before copy password.
Intent intent = new Intent(this, NotificationCopyingService.class);
intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION);
intent.putExtra(NotificationCopyingService.EXTRA_PASSWORD, mEntry.getPassword());
if (mEntry.getUsername().length() > 0) {
intent.putExtra(NotificationCopyingService.EXTRA_USERNAME, mEntry.getUsername());
}
if (mEntry.getTitle() != null)
intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle());
startService(intent);
}
}
}
private void populateTitle(Drawable drawIcon, String text) {
titleIconView.setImageDrawable(drawIcon);
@@ -219,22 +170,16 @@ public class EntryActivity extends LockingHideActivity {
// Assign basic fields
entryContentsView.assignUserName(mEntry.getUsername(true, pm));
entryContentsView.assignUserNameCopyListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
timeoutCopyToClipboard(mEntry.getUsername(true, App.getDB().pm),
getString(R.string.copy_field, getString(R.string.entry_user_name)));
}
});
entryContentsView.assignUserNameCopyListener(view ->
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(true, App.getDB().pm),
getString(R.string.copy_field, getString(R.string.entry_user_name)))
);
entryContentsView.assignPassword(mEntry.getPassword(true, pm));
entryContentsView.assignPasswordCopyListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
timeoutCopyToClipboard(mEntry.getPassword(true, App.getDB().pm),
getString(R.string.copy_field, getString(R.string.entry_password)));
}
});
entryContentsView.assignPasswordCopyListener(view ->
clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(true, App.getDB().pm),
getString(R.string.copy_field, getString(R.string.entry_password)))
);
entryContentsView.assignURL(mEntry.getUrl(true, pm));
@@ -247,12 +192,8 @@ public class EntryActivity extends LockingHideActivity {
for (Map.Entry<String, String> field : mEntry.getExtraFields(pm).entrySet()) {
final String label = field.getKey();
final String value = field.getValue();
entryContentsView.addExtraField(label, value, new View.OnClickListener() {
@Override
public void onClick(View view) {
timeoutCopyToClipboard(value, getString(R.string.copy_field, label));
}
});
entryContentsView.addExtraField(label, value, view ->
clipboardHelper.timeoutCopyToClipboard(value, getString(R.string.copy_field, label)));
}
}
@@ -363,30 +304,6 @@ public class EntryActivity extends LockingHideActivity {
return super.onOptionsItemSelected(item);
}
private void timeoutCopyToClipboard(String text) {
timeoutCopyToClipboard(text, "");
}
private void timeoutCopyToClipboard(String text, String toastString) {
if (!toastString.isEmpty())
Toast.makeText(EntryActivity.this, toastString, Toast.LENGTH_LONG).show();
try {
Util.copyToClipboard(this, text);
} catch (SamsungClipboardException e) {
showSamsungDialog();
return;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String sClipClear = prefs.getString(getString(R.string.clipboard_timeout_key), getString(R.string.clipboard_timeout_default));
long clipClearTime = Long.parseLong(sClipClear);
if ( clipClearTime > 0 ) {
mTimer.schedule(new ClearClipboardTask(this, text), clipClearTime);
}
}
@Override
public void finish() {
@@ -399,55 +316,4 @@ public class EntryActivity extends LockingHideActivity {
*/
super.finish();
}
// Setup to allow the toast to happen in the foreground
final Handler uiThreadCallback = new Handler();
// Task which clears the clipboard, and sends a toast to the foreground.
private class ClearClipboardTask extends TimerTask {
private final String mClearText;
private final Context mCtx;
ClearClipboardTask(Context ctx, String clearText) {
mClearText = clearText;
mCtx = ctx;
}
@Override
public void run() {
String currentClip = Util.getClipboard(mCtx);
if ( currentClip.equals(mClearText) ) {
try {
Util.copyToClipboard(mCtx, "");
uiThreadCallback.post(new UIToastTask(mCtx, R.string.ClearClipboard));
} catch (SamsungClipboardException e) {
uiThreadCallback.post(new UIToastTask(mCtx, R.string.clipboard_error_clear));
}
}
}
}
private void showSamsungDialog() {
String text = getString(R.string.clipboard_error).concat(System.getProperty("line.separator")).concat(getString(R.string.clipboard_error_url));
SpannableString s = new SpannableString(text);
TextView tv = new TextView(this);
tv.setText(s);
tv.setAutoLinkMask(RESULT_OK);
tv.setMovementMethod(LinkMovementMethod.getInstance());
Linkify.addLinks(s, Linkify.WEB_URLS);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.clipboard_error_title)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setView(tv)
.show();
}
}

View File

@@ -49,6 +49,7 @@ public abstract class LockingActivity extends StylishActivity {
@Override
protected void onResume() {
super.onResume();
// TODO Solve timeout shutdown
TimeoutHelper.checkShutdown(this);
TimeoutHelper.recordTime(this);
}

View File

@@ -5,8 +5,6 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
@@ -15,6 +13,8 @@ import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.keepassdroid.database.exception.SamsungClipboardException;
import com.keepassdroid.timeout.ClipboardHelper;
import com.kunzisoft.keepass.R;
import java.util.ArrayList;
@@ -39,7 +39,7 @@ public class NotificationCopyingService extends Service {
public static final String ACTION_CLEAN_CLIPBOARD = "ACTION_CLEAN_CLIPBOARD";
private NotificationManager notificationManager;
private ClipboardManager clipboardManager;
private ClipboardHelper clipboardHelper;
private Thread cleanNotificationTimer;
private Thread countingDownTask;
private int notificationId = 1;
@@ -57,7 +57,7 @@ public class NotificationCopyingService extends Service {
public void onCreate() {
super.onCreate();
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboardHelper = new ClipboardHelper(this);
// Create notification channel for Oreo+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -98,8 +98,11 @@ public class NotificationCopyingService extends Service {
} else if (ACTION_CLEAN_CLIPBOARD.equals(intent.getAction())) {
stopTask(countingDownTask);
cleanPassword();
try {
clipboardHelper.cleanClipboard();
} catch (SamsungClipboardException e) {
Log.e(TAG, "Clipboard can't be cleaned", e);
}
} else {
Log.w(TAG, "unknown action");
}
@@ -159,19 +162,11 @@ public class NotificationCopyingService extends Service {
this, 0, copyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private void addActionsToBuilder(NotificationCompat.Builder builder, List<Field> fieldsToAdd) {
// Add extra actions
for (Field fieldToAdd : fieldsToAdd) {
builder.addAction(R.drawable.notify, fieldToAdd.label,
getCopyPendingIntent(fieldToAdd, fieldsToAdd));
}
}
private void newNotification(@Nullable String title, List<Field> fieldsToAdd) {
stopTask(countingDownTask);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID_COPYING)
.setSmallIcon(R.drawable.notify);
.setSmallIcon(R.drawable.ic_key_white_24dp);
if (title != null)
builder.setContentTitle(title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
@@ -182,8 +177,14 @@ public class NotificationCopyingService extends Service {
builder.setContentText(field.copyText);
builder.setContentIntent(getCopyPendingIntent(field, fieldsToAdd));
// Add extra actions without password
List<Field> fieldsWithoutPassword = new ArrayList<>(fieldsToAdd);
fieldsWithoutPassword.remove(field);
// Add extra actions
addActionsToBuilder(builder, fieldsToAdd);
for (Field fieldToAdd : fieldsWithoutPassword) {
builder.addAction(R.drawable.ic_key_white_24dp, fieldToAdd.label,
getCopyPendingIntent(fieldToAdd, fieldsToAdd));
}
}
notificationManager.cancel(notificationId);
@@ -207,59 +208,61 @@ public class NotificationCopyingService extends Service {
stopTask(countingDownTask);
stopTask(cleanNotificationTimer);
copyToClipboard(fieldToCopy.label, fieldToCopy.value);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID_COPYING)
.setSmallIcon(R.drawable.ic_key_white_24dp)
.setContentTitle(fieldToCopy.label);
try {
clipboardHelper.copyToClipboard(fieldToCopy.label, fieldToCopy.value);
// Remove the current field from the next fields
if (nextFields.contains(fieldToCopy))
nextFields.remove(fieldToCopy);
// New action with next field if click
if (nextFields.size() > 0) {
Field nextField = nextFields.get(0);
builder.setContentText(nextField.copyText);
builder.setContentIntent(getCopyPendingIntent(nextField, nextFields));
// Else tell to swipe for a clean
} else {
builder.setContentText(getString(R.string.clipboard_swipe_clean));
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID_COPYING)
.setSmallIcon(R.drawable.ic_key_white_24dp)
.setContentTitle(fieldToCopy.label);
Intent cleanIntent = new Intent(this, NotificationCopyingService.class);
cleanIntent.setAction(ACTION_CLEAN_CLIPBOARD);
PendingIntent cleanPendingIntent = PendingIntent.getService(
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(cleanPendingIntent);
int myNotificationId = notificationId;
countingDownTask = new Thread(() -> {
int maxPos = 100;
long posDurationMills = notificationTimeoutMilliSecs / maxPos;
for (int pos = maxPos; pos > 0; --pos) {
builder.setProgress(maxPos, pos, false);
notificationManager.notify(myNotificationId, builder.build());
try {
Thread.sleep(posDurationMills);
} catch (InterruptedException e) {
break;
}
// Remove the current field from the next fields
if (nextFields.contains(fieldToCopy))
nextFields.remove(fieldToCopy);
// New action with next field if click
if (nextFields.size() > 0) {
Field nextField = nextFields.get(0);
builder.setContentText(nextField.copyText);
builder.setContentIntent(getCopyPendingIntent(nextField, nextFields));
// Else tell to swipe for a clean
} else {
builder.setContentText(getString(R.string.clipboard_swipe_clean));
}
countingDownTask = null;
notificationManager.cancel(myNotificationId);
// Clean password only if no next field
if (nextFields.size() <= 0)
cleanPassword();
});
countingDownTask.start();
}
private void copyToClipboard(String name, String value) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(name, value));
}
Intent cleanIntent = new Intent(this, NotificationCopyingService.class);
cleanIntent.setAction(ACTION_CLEAN_CLIPBOARD);
PendingIntent cleanPendingIntent = PendingIntent.getService(
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(cleanPendingIntent);
private void cleanPassword() {
clipboardManager.setPrimaryClip(ClipData.newPlainText("",""));
int myNotificationId = notificationId;
countingDownTask = new Thread(() -> {
int maxPos = 100;
long posDurationMills = notificationTimeoutMilliSecs / maxPos;
for (int pos = maxPos; pos > 0; --pos) {
builder.setProgress(maxPos, pos, false);
notificationManager.notify(myNotificationId, builder.build());
try {
Thread.sleep(posDurationMills);
} catch (InterruptedException e) {
break;
}
}
countingDownTask = null;
notificationManager.cancel(myNotificationId);
// Clean password only if no next field
if (nextFields.size() <= 0)
try {
clipboardHelper.cleanClipboard();
} catch (SamsungClipboardException e) {
Log.e(TAG, "Clipboard can't be cleaned", e);
}
});
countingDownTask.start();
} catch (SamsungClipboardException e) {
Log.e(TAG, "Clipboard can't be populate", e);
}
}
private void stopTask(Thread task) {

View File

@@ -0,0 +1,158 @@
/*
*
* Copyright 2018 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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 2 of the License, or
* (at your option) any later version.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.timeout;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.widget.TextView;
import android.widget.Toast;
import com.keepassdroid.database.exception.SamsungClipboardException;
import com.keepassdroid.tasks.UIToastTask;
import com.kunzisoft.keepass.R;
import java.util.Timer;
import java.util.TimerTask;
public class ClipboardHelper {
private Context context;
private ClipboardManager clipboardManager;
private Timer mTimer = new Timer();
public ClipboardHelper(Context context) {
this.context = context;
this.clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
}
public void timeoutCopyToClipboard(String text) {
timeoutCopyToClipboard(text, "");
}
public void timeoutCopyToClipboard(String text, String toastString) {
if (!toastString.isEmpty())
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show();
try {
copyToClipboard(text);
} catch (SamsungClipboardException e) {
showSamsungDialog();
return;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key),
context.getString(R.string.clipboard_timeout_default));
long clipClearTime = Long.parseLong(sClipClear);
if ( clipClearTime > 0 ) {
mTimer.schedule(new ClearClipboardTask(context, text), clipClearTime);
}
}
public CharSequence getClipboard(Context context) {
if (clipboardManager.hasPrimaryClip()) {
ClipData data = clipboardManager.getPrimaryClip();
if (data.getItemCount() > 0) {
CharSequence text = data.getItemAt(0).coerceToText(context);
if (text != null) {
return text;
}
}
}
return "";
}
public void copyToClipboard(String value) throws SamsungClipboardException {
copyToClipboard("", value);
}
public void copyToClipboard(String label, String value) throws SamsungClipboardException {
try {
clipboardManager.setPrimaryClip(ClipData.newPlainText(label, value));
} catch (NullPointerException e) {
throw new SamsungClipboardException(e);
}
}
public void cleanClipboard() throws SamsungClipboardException {
cleanClipboard("");
}
public void cleanClipboard(String label) throws SamsungClipboardException {
copyToClipboard(label,"");
}
// Setup to allow the toast to happen in the foreground
private final Handler uiThreadCallback = new Handler();
// Task which clears the clipboard, and sends a toast to the foreground.
private class ClearClipboardTask extends TimerTask {
private final String mClearText;
private final Context mCtx;
ClearClipboardTask(Context ctx, String clearText) {
mClearText = clearText;
mCtx = ctx;
}
@Override
public void run() {
String currentClip = getClipboard(mCtx).toString();
if ( currentClip.equals(mClearText) ) {
try {
cleanClipboard();
uiThreadCallback.post(new UIToastTask(mCtx, R.string.ClearClipboard));
} catch (SamsungClipboardException e) {
uiThreadCallback.post(new UIToastTask(mCtx, R.string.clipboard_error_clear));
}
}
}
}
private void showSamsungDialog() {
String text = context.getString(R.string.clipboard_error).concat(System.getProperty("line.separator"))
.concat(context.getString(R.string.clipboard_error_url));
SpannableString s = new SpannableString(text);
TextView tv = new TextView(context);
tv.setText(s);
tv.setAutoLinkMask(Activity.RESULT_OK);
tv.setMovementMethod(LinkMovementMethod.getInstance());
Linkify.addLinks(s, Linkify.WEB_URLS);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.clipboard_error_title)
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.setView(tv)
.show();
}
}

View File

@@ -89,7 +89,7 @@ public class TimeoutHelper {
public static void checkShutdown(Activity act) {
if ( App.isShutdown() && App.getDB().Loaded() ) {
Log.e(TAG, "Shutdown " + act.getLocalClassName() + " after inactivity");
Log.i(TAG, "Shutdown " + act.getLocalClassName() + " after inactivity");
act.setResult(PasswordActivity.RESULT_EXIT_LOCK);
act.finish();
}

View File

@@ -20,40 +20,18 @@
package com.keepassdroid.utils;
import android.content.ActivityNotFoundException;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.widget.TextView;
import com.keepassdroid.database.exception.SamsungClipboardException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class Util {
public static String getClipboard(Context context) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
CharSequence csText = clipboard.getText();
if (csText == null) {
return "";
}
return csText.toString();
}
public static void copyToClipboard(Context context, String text) throws SamsungClipboardException {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
try {
clipboard.setText(text);
} catch (NullPointerException e) {
throw new SamsungClipboardException(e);
}
}
public static void gotoUrl(Context context, String url) throws ActivityNotFoundException {
if ( url != null && url.length() > 0 ) {
Uri uri = Uri.parse(url);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB