Merge "Updates to "Displaying Bitmaps Efficiently" class. Changes: -Updated code sample (see http://ag/214812) -Updated code snippets to match updated sample -Fixed <> in code snippets -Updated disk cache section -Some other minor updates" into jb-dev
This commit is contained in:
Binary file not shown.
@ -96,7 +96,7 @@ and leave the rest of your app little memory to work with.</p>
|
|||||||
<p>Here’s an example of setting up a {@link android.util.LruCache} for bitmaps:</p>
|
<p>Here’s an example of setting up a {@link android.util.LruCache} for bitmaps:</p>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
private LruCache<String, Bitmap> mMemoryCache;
|
private LruCache<String, Bitmap> mMemoryCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@ -109,7 +109,7 @@ protected void onCreate(Bundle savedInstanceState) {
|
|||||||
// Use 1/8th of the available memory for this memory cache.
|
// Use 1/8th of the available memory for this memory cache.
|
||||||
final int cacheSize = 1024 * 1024 * memClass / 8;
|
final int cacheSize = 1024 * 1024 * memClass / 8;
|
||||||
|
|
||||||
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
|
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
|
||||||
@Override
|
@Override
|
||||||
protected int sizeOf(String key, Bitmap bitmap) {
|
protected int sizeOf(String key, Bitmap bitmap) {
|
||||||
// The cache size will be measured in bytes rather than number of items.
|
// The cache size will be measured in bytes rather than number of items.
|
||||||
@ -159,7 +159,7 @@ public void loadBitmap(int resId, ImageView imageView) {
|
|||||||
updated to add entries to the memory cache:</p>
|
updated to add entries to the memory cache:</p>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
||||||
...
|
...
|
||||||
// Decode image in background.
|
// Decode image in background.
|
||||||
@Override
|
@Override
|
||||||
@ -179,7 +179,7 @@ class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
|||||||
rely on images being available in this cache. Components like {@link android.widget.GridView} with
|
rely on images being available in this cache. Components like {@link android.widget.GridView} with
|
||||||
larger datasets can easily fill up a memory cache. Your application could be interrupted by another
|
larger datasets can easily fill up a memory cache. Your application could be interrupted by another
|
||||||
task like a phone call, and while in the background it might be killed and the memory cache
|
task like a phone call, and while in the background it might be killed and the memory cache
|
||||||
destroyed. Once the user resumes, your application it has to process each image again.</p>
|
destroyed. Once the user resumes, your application has to process each image again.</p>
|
||||||
|
|
||||||
<p>A disk cache can be used in these cases to persist processed bitmaps and help decrease loading
|
<p>A disk cache can be used in these cases to persist processed bitmaps and help decrease loading
|
||||||
times where images are no longer available in a memory cache. Of course, fetching images from disk
|
times where images are no longer available in a memory cache. Of course, fetching images from disk
|
||||||
@ -190,18 +190,14 @@ be unpredictable.</p>
|
|||||||
appropriate place to store cached images if they are accessed more frequently, for example in an
|
appropriate place to store cached images if they are accessed more frequently, for example in an
|
||||||
image gallery application.</p>
|
image gallery application.</p>
|
||||||
|
|
||||||
<p>Included in the sample code of this class is a basic {@code DiskLruCache} implementation.
|
<p>The sample code of this class uses a {@code DiskLruCache} implementation that is pulled from the
|
||||||
However, a more robust and recommended {@code DiskLruCache} solution is included in the Android 4.0
|
<a href="https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/io/DiskLruCache.java">Android source</a>. Here’s updated example code that adds a disk cache in addition
|
||||||
source code ({@code libcore/luni/src/main/java/libcore/io/DiskLruCache.java}). Back-porting this
|
to the existing memory cache:</p>
|
||||||
class for use on previous Android releases should be fairly straightforward (a <a
|
|
||||||
href="http://www.google.com/search?q=disklrucache">quick search</a> shows others who have already
|
|
||||||
implemented this solution).</p>
|
|
||||||
|
|
||||||
<p>Here’s updated example code that uses the simple {@code DiskLruCache} included in the sample
|
|
||||||
application of this class:</p>
|
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
private DiskLruCache mDiskCache;
|
private DiskLruCache mDiskLruCache;
|
||||||
|
private final Object mDiskCacheLock = new Object();
|
||||||
|
private boolean mDiskCacheStarting = true;
|
||||||
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
|
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
|
||||||
private static final String DISK_CACHE_SUBDIR = "thumbnails";
|
private static final String DISK_CACHE_SUBDIR = "thumbnails";
|
||||||
|
|
||||||
@ -210,12 +206,26 @@ protected void onCreate(Bundle savedInstanceState) {
|
|||||||
...
|
...
|
||||||
// Initialize memory cache
|
// Initialize memory cache
|
||||||
...
|
...
|
||||||
File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
|
// Initialize disk cache on background thread
|
||||||
mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
|
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
|
||||||
|
new InitDiskCacheTask().execute(cacheDir);
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(File... params) {
|
||||||
|
synchronized (mDiskCacheLock) {
|
||||||
|
File cacheDir = params[0];
|
||||||
|
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
|
||||||
|
mDiskCacheStarting = false; // Finished initialization
|
||||||
|
mDiskCacheLock.notifyAll(); // Wake any waiting threads
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
||||||
...
|
...
|
||||||
// Decode image in background.
|
// Decode image in background.
|
||||||
@Override
|
@Override
|
||||||
@ -232,7 +242,7 @@ class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add final bitmap to caches
|
// Add final bitmap to caches
|
||||||
addBitmapToCache(String.valueOf(imageKey, bitmap);
|
addBitmapToCache(imageKey, bitmap);
|
||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
@ -246,28 +256,48 @@ public void addBitmapToCache(String key, Bitmap bitmap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Also add to disk cache
|
// Also add to disk cache
|
||||||
if (!mDiskCache.containsKey(key)) {
|
synchronized (mDiskCacheLock) {
|
||||||
mDiskCache.put(key, bitmap);
|
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
|
||||||
|
mDiskLruCache.put(key, bitmap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap getBitmapFromDiskCache(String key) {
|
public Bitmap getBitmapFromDiskCache(String key) {
|
||||||
return mDiskCache.get(key);
|
synchronized (mDiskCacheLock) {
|
||||||
|
// Wait while disk cache is started from background thread
|
||||||
|
while (mDiskCacheStarting) {
|
||||||
|
try {
|
||||||
|
mDiskCacheLock.wait();
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
if (mDiskLruCache != null) {
|
||||||
|
return mDiskLruCache.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
|
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
|
||||||
// but if not mounted, falls back on internal storage.
|
// but if not mounted, falls back on internal storage.
|
||||||
public static File getCacheDir(Context context, String uniqueName) {
|
public static File getDiskCacheDir(Context context, String uniqueName) {
|
||||||
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
|
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
|
||||||
// otherwise use internal cache dir
|
// otherwise use internal cache dir
|
||||||
final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
|
final String cachePath =
|
||||||
|| !Environment.isExternalStorageRemovable() ?
|
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
|
||||||
context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
|
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
|
||||||
|
context.getCacheDir().getPath();
|
||||||
|
|
||||||
return new File(cachePath + File.separator + uniqueName);
|
return new File(cachePath + File.separator + uniqueName);
|
||||||
}
|
}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
<p class="note"><strong>Note:</strong> Even initializing the disk cache requires disk operations
|
||||||
|
and therefore should not take place on the main thread. However, this does mean there's a chance
|
||||||
|
the cache is accessed before initialization. To address this, in the above implementation, a lock
|
||||||
|
object ensures that the app does not read from the disk cache until the cache has been
|
||||||
|
initialized.</p>
|
||||||
|
|
||||||
<p>While the memory cache is checked in the UI thread, the disk cache is checked in the background
|
<p>While the memory cache is checked in the UI thread, the disk cache is checked in the background
|
||||||
thread. Disk operations should never take place on the UI thread. When image processing is
|
thread. Disk operations should never take place on the UI thread. When image processing is
|
||||||
complete, the final bitmap is added to both the memory and disk cache for future use.</p>
|
complete, the final bitmap is added to both the memory and disk cache for future use.</p>
|
||||||
@ -292,7 +322,7 @@ android.widget.ImageView} objects.</p>
|
|||||||
changes using a {@link android.app.Fragment}:</p>
|
changes using a {@link android.app.Fragment}:</p>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
private LruCache<String, Bitmap> mMemoryCache;
|
private LruCache<String, Bitmap> mMemoryCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@ -301,7 +331,7 @@ protected void onCreate(Bundle savedInstanceState) {
|
|||||||
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
|
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
|
||||||
mMemoryCache = RetainFragment.mRetainedCache;
|
mMemoryCache = RetainFragment.mRetainedCache;
|
||||||
if (mMemoryCache == null) {
|
if (mMemoryCache == null) {
|
||||||
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
|
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
|
||||||
... // Initialize cache here as usual
|
... // Initialize cache here as usual
|
||||||
}
|
}
|
||||||
mRetainFragment.mRetainedCache = mMemoryCache;
|
mRetainFragment.mRetainedCache = mMemoryCache;
|
||||||
@ -311,7 +341,7 @@ protected void onCreate(Bundle savedInstanceState) {
|
|||||||
|
|
||||||
class RetainFragment extends Fragment {
|
class RetainFragment extends Fragment {
|
||||||
private static final String TAG = "RetainFragment";
|
private static final String TAG = "RetainFragment";
|
||||||
public LruCache<String, Bitmap> mRetainedCache;
|
public LruCache<String, Bitmap> mRetainedCache;
|
||||||
|
|
||||||
public RetainFragment() {}
|
public RetainFragment() {}
|
||||||
|
|
||||||
|
@ -103,7 +103,8 @@ public class ImageDetailActivity extends FragmentActivity {
|
|||||||
}
|
}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<p>The details {@link android.app.Fragment} holds the {@link android.widget.ImageView} children:</p>
|
<p>Here is an implementation of the details {@link android.app.Fragment} which holds the {@link android.widget.ImageView} children. This might seem like a perfectly reasonable approach, but can
|
||||||
|
you see the drawbacks of this implementation? How could it be improved?</p>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
public class ImageDetailFragment extends Fragment {
|
public class ImageDetailFragment extends Fragment {
|
||||||
@ -146,11 +147,11 @@ public class ImageDetailFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<p>Hopefully you noticed the issue with this implementation; The images are being read from
|
<p>Hopefully you noticed the issue: the images are being read from resources on the UI thread,
|
||||||
resources on the UI thread which can lead to an application hanging and being force closed. Using an
|
which can lead to an application hanging and being force closed. Using an
|
||||||
{@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps Off
|
{@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps
|
||||||
the UI Thread</a> lesson, it’s straightforward to move image loading and processing to a background
|
Off the UI Thread</a> lesson, it’s straightforward to move image loading and processing to a
|
||||||
thread:</p>
|
background thread:</p>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
public class ImageDetailActivity extends FragmentActivity {
|
public class ImageDetailActivity extends FragmentActivity {
|
||||||
@ -190,7 +191,7 @@ modifications for a memory cache:</p>
|
|||||||
<pre>
|
<pre>
|
||||||
public class ImageDetailActivity extends FragmentActivity {
|
public class ImageDetailActivity extends FragmentActivity {
|
||||||
...
|
...
|
||||||
private LruCache<String, Bitmap> mMemoryCache;
|
private LruCache<String, Bitmap> mMemoryCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
@ -229,7 +230,8 @@ UI remains fluid, memory usage remains under control and concurrency is handled
|
|||||||
the way {@link android.widget.GridView} recycles its children views).</p>
|
the way {@link android.widget.GridView} recycles its children views).</p>
|
||||||
|
|
||||||
<p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link
|
<p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link
|
||||||
android.widget.ImageView} children placed inside a {@link android.app.Fragment}:</p>
|
android.widget.ImageView} children placed inside a {@link android.app.Fragment}. Again, this might
|
||||||
|
seem like a perfectly reasonable approach, but what would make it better?</p>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
|
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
|
||||||
@ -261,7 +263,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
||||||
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
|
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
|
||||||
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
|
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
@ -345,13 +347,13 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
static class AsyncDrawable extends BitmapDrawable {
|
static class AsyncDrawable extends BitmapDrawable {
|
||||||
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
|
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
|
||||||
|
|
||||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
BitmapWorkerTask bitmapWorkerTask) {
|
BitmapWorkerTask bitmapWorkerTask) {
|
||||||
super(res, bitmap);
|
super(res, bitmap);
|
||||||
bitmapWorkerTaskReference =
|
bitmapWorkerTaskReference =
|
||||||
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
|
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitmapWorkerTask getBitmapWorkerTask() {
|
public BitmapWorkerTask getBitmapWorkerTask() {
|
||||||
|
@ -43,8 +43,8 @@ exception:<br />{@code java.lang.OutofMemoryError: bitmap size exceeds VM budget
|
|||||||
perform under this minimum memory limit. However, keep in mind many devices are configured with
|
perform under this minimum memory limit. However, keep in mind many devices are configured with
|
||||||
higher limits.</li>
|
higher limits.</li>
|
||||||
<li>Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the
|
<li>Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the
|
||||||
camera on the <a href="http://www.google.com/nexus/">Galaxy Nexus</a> takes photos up to 2592x1936
|
camera on the <a href="http://www.android.com/devices/detail/galaxy-nexus">Galaxy Nexus</a> takes
|
||||||
pixels (5 megapixels). If the bitmap configuration used is {@link
|
photos up to 2592x1936 pixels (5 megapixels). If the bitmap configuration used is {@link
|
||||||
android.graphics.Bitmap.Config ARGB_8888} (the default from the Android 2.3 onward) then loading
|
android.graphics.Bitmap.Config ARGB_8888} (the default from the Android 2.3 onward) then loading
|
||||||
this image into memory takes about 19MB of memory (2592*1936*4 bytes), immediately exhausting the
|
this image into memory takes about 19MB of memory (2592*1936*4 bytes), immediately exhausting the
|
||||||
per-app limit on some devices.</li>
|
per-app limit on some devices.</li>
|
||||||
@ -75,4 +75,4 @@ exception:<br />{@code java.lang.OutofMemoryError: bitmap size exceeds VM budget
|
|||||||
components like {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
|
components like {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
|
||||||
using a background thread and bitmap cache.</dd>
|
using a background thread and bitmap cache.</dd>
|
||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
|
@ -62,13 +62,13 @@ decodeSampledBitmapFromResource()}</a>: </p>
|
|||||||
|
|
||||||
<a name="BitmapWorkerTask"></a>
|
<a name="BitmapWorkerTask"></a>
|
||||||
<pre>
|
<pre>
|
||||||
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
||||||
private final WeakReference<ImageView> imageViewReference;
|
private final WeakReference<ImageView> imageViewReference;
|
||||||
private int data = 0;
|
private int data = 0;
|
||||||
|
|
||||||
public BitmapWorkerTask(ImageView imageView) {
|
public BitmapWorkerTask(ImageView imageView) {
|
||||||
// Use a WeakReference to ensure the ImageView can be garbage collected
|
// Use a WeakReference to ensure the ImageView can be garbage collected
|
||||||
imageViewReference = new WeakReference<ImageView>(imageView);
|
imageViewReference = new WeakReference<ImageView>(imageView);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode image in background.
|
// Decode image in background.
|
||||||
@ -133,13 +133,13 @@ completes:</p>
|
|||||||
<a name="AsyncDrawable"></a>
|
<a name="AsyncDrawable"></a>
|
||||||
<pre>
|
<pre>
|
||||||
static class AsyncDrawable extends BitmapDrawable {
|
static class AsyncDrawable extends BitmapDrawable {
|
||||||
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
|
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
|
||||||
|
|
||||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
BitmapWorkerTask bitmapWorkerTask) {
|
BitmapWorkerTask bitmapWorkerTask) {
|
||||||
super(res, bitmap);
|
super(res, bitmap);
|
||||||
bitmapWorkerTaskReference =
|
bitmapWorkerTaskReference =
|
||||||
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
|
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitmapWorkerTask getBitmapWorkerTask() {
|
public BitmapWorkerTask getBitmapWorkerTask() {
|
||||||
@ -211,7 +211,7 @@ one associated with the {@link android.widget.ImageView}:</p>
|
|||||||
|
|
||||||
<a name="BitmapWorkerTaskUpdated"></a>
|
<a name="BitmapWorkerTaskUpdated"></a>
|
||||||
<pre>
|
<pre>
|
||||||
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
||||||
...
|
...
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -236,4 +236,4 @@ class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
|
|||||||
android.widget.GridView} components as well as any other components that recycle their child
|
android.widget.GridView} components as well as any other components that recycle their child
|
||||||
views. Simply call {@code loadBitmap} where you normally set an image to your {@link
|
views. Simply call {@code loadBitmap} where you normally set an image to your {@link
|
||||||
android.widget.ImageView}. For example, in a {@link android.widget.GridView} implementation this
|
android.widget.ImageView}. For example, in a {@link android.widget.GridView} implementation this
|
||||||
would be in the {@link android.widget.Adapter#getView getView()} method of the backing adapter.</p>
|
would be in the {@link android.widget.Adapter#getView getView()} method of the backing adapter.</p>
|
||||||
|
Reference in New Issue
Block a user