• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.contacts.common;
18 
19 import android.app.ActivityManager;
20 import android.content.ComponentCallbacks2;
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.database.Cursor;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Paint;
30 import android.graphics.Paint.Style;
31 import android.graphics.drawable.BitmapDrawable;
32 import android.graphics.drawable.Drawable;
33 import android.graphics.drawable.TransitionDrawable;
34 import android.media.ThumbnailUtils;
35 import android.net.TrafficStats;
36 import android.net.Uri;
37 import android.os.Handler;
38 import android.os.Handler.Callback;
39 import android.os.HandlerThread;
40 import android.os.Message;
41 import android.provider.ContactsContract;
42 import android.provider.ContactsContract.Contacts;
43 import android.provider.ContactsContract.Contacts.Photo;
44 import android.provider.ContactsContract.Data;
45 import android.provider.ContactsContract.Directory;
46 import android.support.annotation.UiThread;
47 import android.support.annotation.WorkerThread;
48 import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
49 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
50 import android.text.TextUtils;
51 import android.util.LruCache;
52 import android.view.View;
53 import android.view.ViewGroup;
54 import android.widget.ImageView;
55 import com.android.contacts.common.util.BitmapUtil;
56 import com.android.contacts.common.util.TrafficStatsTags;
57 import com.android.contacts.common.util.UriUtils;
58 import com.android.dialer.common.LogUtil;
59 import com.android.dialer.util.PermissionsUtil;
60 import java.io.ByteArrayOutputStream;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.lang.ref.Reference;
64 import java.lang.ref.SoftReference;
65 import java.net.HttpURLConnection;
66 import java.net.URL;
67 import java.util.ArrayList;
68 import java.util.HashSet;
69 import java.util.Iterator;
70 import java.util.List;
71 import java.util.Map.Entry;
72 import java.util.Set;
73 import java.util.concurrent.ConcurrentHashMap;
74 import java.util.concurrent.atomic.AtomicInteger;
75 
76 class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
77 
78   private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
79 
80   private static final int FADE_TRANSITION_DURATION = 200;
81 
82   /**
83    * Type of message sent by the UI thread to itself to indicate that some photos need to be loaded.
84    */
85   private static final int MESSAGE_REQUEST_LOADING = 1;
86 
87   /** Type of message sent by the loader thread to indicate that some photos have been loaded. */
88   private static final int MESSAGE_PHOTOS_LOADED = 2;
89 
90   private static final String[] EMPTY_STRING_ARRAY = new String[0];
91 
92   private static final String[] COLUMNS = new String[] {Photo._ID, Photo.PHOTO};
93 
94   /**
95    * Dummy object used to indicate that a bitmap for a given key could not be stored in the cache.
96    */
97   private static final BitmapHolder BITMAP_UNAVAILABLE;
98   /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
99   private static final int HOLDER_CACHE_SIZE = 2000000;
100   /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
101   private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
102   /** Height/width of a thumbnail image */
103   private static int mThumbnailSize;
104 
105   static {
106     BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0);
107     BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null);
108   }
109 
110   private final Context mContext;
111   /**
112    * An LRU cache for bitmap holders. The cache contains bytes for photos just as they come from the
113    * database. Each holder has a soft reference to the actual bitmap.
114    */
115   private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
116   /** Cache size threshold at which bitmaps will not be preloaded. */
117   private final int mBitmapHolderCacheRedZoneBytes;
118   /**
119    * Level 2 LRU cache for bitmaps. This is a smaller cache that holds the most recently used
120    * bitmaps to save time on decoding them from bytes (the bytes are stored in {@link
121    * #mBitmapHolderCache}.
122    */
123   private final LruCache<Object, Bitmap> mBitmapCache;
124   /**
125    * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. The
126    * request may swapped out before the photo loading request is started.
127    */
128   private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
129       new ConcurrentHashMap<ImageView, Request>();
130   /** Handler for messages sent to the UI thread. */
131   private final Handler mMainThreadHandler = new Handler(this);
132   /** For debug: How many times we had to reload cached photo for a stale entry */
133   private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
134   /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
135   private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
136   /** {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. */
137   private volatile boolean mBitmapHolderCacheAllUnfresh = true;
138   /** Thread responsible for loading photos from the database. Created upon the first request. */
139   private LoaderThread mLoaderThread;
140   /** A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */
141   private boolean mLoadingRequested;
142   /** Flag indicating if the image loading is paused. */
143   private boolean mPaused;
144   /** The user agent string to use when loading URI based photos. */
145   private String mUserAgent;
146 
ContactPhotoManagerImpl(Context context)147   public ContactPhotoManagerImpl(Context context) {
148     mContext = context;
149 
150     final ActivityManager am =
151         ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE));
152 
153     final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f;
154 
155     final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
156     mBitmapCache =
157         new LruCache<Object, Bitmap>(bitmapCacheSize) {
158           @Override
159           protected int sizeOf(Object key, Bitmap value) {
160             return value.getByteCount();
161           }
162 
163           @Override
164           protected void entryRemoved(
165               boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
166             if (DEBUG) {
167               dumpStats();
168             }
169           }
170         };
171     final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
172     mBitmapHolderCache =
173         new LruCache<Object, BitmapHolder>(holderCacheSize) {
174           @Override
175           protected int sizeOf(Object key, BitmapHolder value) {
176             return value.bytes != null ? value.bytes.length : 0;
177           }
178 
179           @Override
180           protected void entryRemoved(
181               boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
182             if (DEBUG) {
183               dumpStats();
184             }
185           }
186         };
187     mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
188     LogUtil.i(
189         "ContactPhotoManagerImpl.ContactPhotoManagerImpl", "cache adj: " + cacheSizeAdjustment);
190     if (DEBUG) {
191       LogUtil.d(
192           "ContactPhotoManagerImpl.ContactPhotoManagerImpl",
193           "Cache size: " + btk(mBitmapHolderCache.maxSize()) + " + " + btk(mBitmapCache.maxSize()));
194     }
195 
196     mThumbnailSize =
197         context.getResources().getDimensionPixelSize(R.dimen.contact_browser_list_item_photo_size);
198 
199     // Get a user agent string to use for URI photo requests.
200     mUserAgent = Bindings.get(context).getUserAgent();
201     if (mUserAgent == null) {
202       mUserAgent = "";
203     }
204   }
205 
206   /** Converts bytes to K bytes, rounding up. Used only for debug log. */
btk(int bytes)207   private static String btk(int bytes) {
208     return ((bytes + 1023) / 1024) + "K";
209   }
210 
safeDiv(int dividend, int divisor)211   private static final int safeDiv(int dividend, int divisor) {
212     return (divisor == 0) ? 0 : (dividend / divisor);
213   }
214 
isChildView(View parent, View potentialChild)215   private static boolean isChildView(View parent, View potentialChild) {
216     return potentialChild.getParent() != null
217         && (potentialChild.getParent() == parent
218             || (potentialChild.getParent() instanceof ViewGroup
219                 && isChildView(parent, (ViewGroup) potentialChild.getParent())));
220   }
221 
222   /**
223    * If necessary, decodes bytes stored in the holder to Bitmap. As long as the bitmap is held
224    * either by {@link #mBitmapCache} or by a soft reference in the holder, it will not be necessary
225    * to decode the bitmap.
226    */
inflateBitmap(BitmapHolder holder, int requestedExtent)227   private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
228     final int sampleSize =
229         BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
230     byte[] bytes = holder.bytes;
231     if (bytes == null || bytes.length == 0) {
232       return;
233     }
234 
235     if (sampleSize == holder.decodedSampleSize) {
236       // Check the soft reference.  If will be retained if the bitmap is also
237       // in the LRU cache, so we don't need to check the LRU cache explicitly.
238       if (holder.bitmapRef != null) {
239         holder.bitmap = holder.bitmapRef.get();
240         if (holder.bitmap != null) {
241           return;
242         }
243       }
244     }
245 
246     try {
247       Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
248 
249       // TODO: As a temporary workaround while framework support is being added to
250       // clip non-square bitmaps into a perfect circle, manually crop the bitmap into
251       // into a square if it will be displayed as a thumbnail so that it can be cropped
252       // into a circle.
253       final int height = bitmap.getHeight();
254       final int width = bitmap.getWidth();
255 
256       // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just
257       // below twice the length of a thumbnail image due to the way we calculate the optimal
258       // sample size.
259       if (height != width && Math.min(height, width) <= mThumbnailSize * 2) {
260         final int dimension = Math.min(height, width);
261         bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
262       }
263       // make bitmap mutable and draw size onto it
264       if (DEBUG_SIZES) {
265         Bitmap original = bitmap;
266         bitmap = bitmap.copy(bitmap.getConfig(), true);
267         original.recycle();
268         Canvas canvas = new Canvas(bitmap);
269         Paint paint = new Paint();
270         paint.setTextSize(16);
271         paint.setColor(Color.BLUE);
272         paint.setStyle(Style.FILL);
273         canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
274         paint.setColor(Color.WHITE);
275         paint.setAntiAlias(true);
276         canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
277       }
278 
279       holder.decodedSampleSize = sampleSize;
280       holder.bitmap = bitmap;
281       holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
282       if (DEBUG) {
283         LogUtil.d(
284             "ContactPhotoManagerImpl.inflateBitmap",
285             "inflateBitmap "
286                 + btk(bytes.length)
287                 + " -> "
288                 + bitmap.getWidth()
289                 + "x"
290                 + bitmap.getHeight()
291                 + ", "
292                 + btk(bitmap.getByteCount()));
293       }
294     } catch (OutOfMemoryError e) {
295       // Do nothing - the photo will appear to be missing
296     }
297   }
298 
299   /** Dump cache stats on logcat. */
dumpStats()300   private void dumpStats() {
301     if (!DEBUG) {
302       return;
303     }
304     {
305       int numHolders = 0;
306       int rawBytes = 0;
307       int bitmapBytes = 0;
308       int numBitmaps = 0;
309       for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
310         numHolders++;
311         if (h.bytes != null) {
312           rawBytes += h.bytes.length;
313         }
314         Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
315         if (b != null) {
316           numBitmaps++;
317           bitmapBytes += b.getByteCount();
318         }
319       }
320       LogUtil.d(
321           "ContactPhotoManagerImpl.dumpStats",
322           "L1: "
323               + btk(rawBytes)
324               + " + "
325               + btk(bitmapBytes)
326               + " = "
327               + btk(rawBytes + bitmapBytes)
328               + ", "
329               + numHolders
330               + " holders, "
331               + numBitmaps
332               + " bitmaps, avg: "
333               + btk(safeDiv(rawBytes, numHolders))
334               + ","
335               + btk(safeDiv(bitmapBytes, numBitmaps)));
336       LogUtil.d(
337           "ContactPhotoManagerImpl.dumpStats",
338           "L1 Stats: "
339               + mBitmapHolderCache.toString()
340               + ", overwrite: fresh="
341               + mFreshCacheOverwrite.get()
342               + " stale="
343               + mStaleCacheOverwrite.get());
344     }
345 
346     {
347       int numBitmaps = 0;
348       int bitmapBytes = 0;
349       for (Bitmap b : mBitmapCache.snapshot().values()) {
350         numBitmaps++;
351         bitmapBytes += b.getByteCount();
352       }
353       LogUtil.d(
354           "ContactPhotoManagerImpl.dumpStats",
355           "L2: "
356               + btk(bitmapBytes)
357               + ", "
358               + numBitmaps
359               + " bitmaps"
360               + ", avg: "
361               + btk(safeDiv(bitmapBytes, numBitmaps)));
362       // We don't get from L2 cache, so L2 stats is meaningless.
363     }
364   }
365 
366   @Override
onTrimMemory(int level)367   public void onTrimMemory(int level) {
368     if (DEBUG) {
369       LogUtil.d("ContactPhotoManagerImpl.onTrimMemory", "onTrimMemory: " + level);
370     }
371     if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
372       // Clear the caches.  Note all pending requests will be removed too.
373       clear();
374     }
375   }
376 
377   @Override
preloadPhotosInBackground()378   public void preloadPhotosInBackground() {
379     ensureLoaderThread();
380     mLoaderThread.requestPreloading();
381   }
382 
383   @Override
loadThumbnail( ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)384   public void loadThumbnail(
385       ImageView view,
386       long photoId,
387       boolean darkTheme,
388       boolean isCircular,
389       DefaultImageRequest defaultImageRequest,
390       DefaultImageProvider defaultProvider) {
391     if (photoId == 0) {
392       // No photo is needed
393       defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest);
394       mPendingRequests.remove(view);
395     } else {
396       if (DEBUG) {
397         LogUtil.d("ContactPhotoManagerImpl.loadThumbnail", "loadPhoto request: " + photoId);
398       }
399       loadPhotoByIdOrUri(
400           view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, defaultProvider));
401     }
402   }
403 
404   @Override
loadPhoto( ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)405   public void loadPhoto(
406       ImageView view,
407       Uri photoUri,
408       int requestedExtent,
409       boolean darkTheme,
410       boolean isCircular,
411       DefaultImageRequest defaultImageRequest,
412       DefaultImageProvider defaultProvider) {
413     if (photoUri == null) {
414       // No photo is needed
415       defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, defaultImageRequest);
416       mPendingRequests.remove(view);
417     } else {
418       if (DEBUG) {
419         LogUtil.d("ContactPhotoManagerImpl.loadPhoto", "loadPhoto request: " + photoUri);
420       }
421       if (isDefaultImageUri(photoUri)) {
422         createAndApplyDefaultImageForUri(
423             view, photoUri, requestedExtent, darkTheme, isCircular, defaultProvider);
424       } else {
425         loadPhotoByIdOrUri(
426             view,
427             Request.createFromUri(
428                 photoUri, requestedExtent, darkTheme, isCircular, defaultProvider));
429       }
430     }
431   }
432 
createAndApplyDefaultImageForUri( ImageView view, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)433   private void createAndApplyDefaultImageForUri(
434       ImageView view,
435       Uri uri,
436       int requestedExtent,
437       boolean darkTheme,
438       boolean isCircular,
439       DefaultImageProvider defaultProvider) {
440     DefaultImageRequest request = getDefaultImageRequestFromUri(uri);
441     request.isCircular = isCircular;
442     defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request);
443   }
444 
loadPhotoByIdOrUri(ImageView view, Request request)445   private void loadPhotoByIdOrUri(ImageView view, Request request) {
446     boolean loaded = loadCachedPhoto(view, request, false);
447     if (loaded) {
448       mPendingRequests.remove(view);
449     } else {
450       mPendingRequests.put(view, request);
451       if (!mPaused) {
452         // Send a request to start loading photos
453         requestLoading();
454       }
455     }
456   }
457 
458   @Override
removePhoto(ImageView view)459   public void removePhoto(ImageView view) {
460     view.setImageDrawable(null);
461     mPendingRequests.remove(view);
462   }
463 
464   /**
465    * Cancels pending requests to load photos asynchronously for views inside {@param
466    * fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests.
467    */
468   @Override
cancelPendingRequests(View fragmentRootView)469   public void cancelPendingRequests(View fragmentRootView) {
470     if (fragmentRootView == null) {
471       mPendingRequests.clear();
472       return;
473     }
474     final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
475     while (iterator.hasNext()) {
476       final ImageView imageView = iterator.next().getKey();
477       // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then
478       // we can safely remove its request.
479       if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) {
480         iterator.remove();
481       }
482     }
483   }
484 
485   @Override
refreshCache()486   public void refreshCache() {
487     if (mBitmapHolderCacheAllUnfresh) {
488       if (DEBUG) {
489         LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache -- no fresh entries.");
490       }
491       return;
492     }
493     if (DEBUG) {
494       LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache");
495     }
496     mBitmapHolderCacheAllUnfresh = true;
497     for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
498       if (holder != BITMAP_UNAVAILABLE) {
499         holder.fresh = false;
500       }
501     }
502   }
503 
504   /**
505    * Checks if the photo is present in cache. If so, sets the photo on the view.
506    *
507    * @return false if the photo needs to be (re)loaded from the provider.
508    */
509   @UiThread
loadCachedPhoto(ImageView view, Request request, boolean fadeIn)510   private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
511     BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
512     if (holder == null) {
513       // The bitmap has not been loaded ==> show default avatar
514       request.applyDefaultImage(view, request.mIsCircular);
515       return false;
516     }
517 
518     if (holder.bytes == null) {
519       request.applyDefaultImage(view, request.mIsCircular);
520       return holder.fresh;
521     }
522 
523     Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
524     if (cachedBitmap == null) {
525       request.applyDefaultImage(view, request.mIsCircular);
526       return false;
527     }
528 
529     final Drawable previousDrawable = view.getDrawable();
530     if (fadeIn && previousDrawable != null) {
531       final Drawable[] layers = new Drawable[2];
532       // Prevent cascade of TransitionDrawables.
533       if (previousDrawable instanceof TransitionDrawable) {
534         final TransitionDrawable previousTransitionDrawable = (TransitionDrawable) previousDrawable;
535         layers[0] =
536             previousTransitionDrawable.getDrawable(
537                 previousTransitionDrawable.getNumberOfLayers() - 1);
538       } else {
539         layers[0] = previousDrawable;
540       }
541       layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
542       TransitionDrawable drawable = new TransitionDrawable(layers);
543       view.setImageDrawable(drawable);
544       drawable.startTransition(FADE_TRANSITION_DURATION);
545     } else {
546       view.setImageDrawable(getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
547     }
548 
549     // Put the bitmap in the LRU cache. But only do this for images that are small enough
550     // (we require that at least six of those can be cached at the same time)
551     if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
552       mBitmapCache.put(request.getKey(), cachedBitmap);
553     }
554 
555     // Soften the reference
556     holder.bitmap = null;
557 
558     return holder.fresh;
559   }
560 
561   /**
562    * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
563    * specified request.
564    */
getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request)565   private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) {
566     if (request.mIsCircular) {
567       final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap);
568       drawable.setAntiAlias(true);
569       drawable.setCornerRadius(bitmap.getHeight() / 2);
570       return drawable;
571     } else {
572       return new BitmapDrawable(resources, bitmap);
573     }
574   }
575 
clear()576   public void clear() {
577     if (DEBUG) {
578       LogUtil.d("ContactPhotoManagerImpl.clear", "clear");
579     }
580     mPendingRequests.clear();
581     mBitmapHolderCache.evictAll();
582     mBitmapCache.evictAll();
583   }
584 
585   @Override
pause()586   public void pause() {
587     mPaused = true;
588   }
589 
590   @Override
resume()591   public void resume() {
592     mPaused = false;
593     if (DEBUG) {
594       dumpStats();
595     }
596     if (!mPendingRequests.isEmpty()) {
597       requestLoading();
598     }
599   }
600 
601   /**
602    * Sends a message to this thread itself to start loading images. If the current view contains
603    * multiple image views, all of those image views will get a chance to request their respective
604    * photos before any of those requests are executed. This allows us to load images in bulk.
605    */
requestLoading()606   private void requestLoading() {
607     if (!mLoadingRequested) {
608       mLoadingRequested = true;
609       mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
610     }
611   }
612 
613   /** Processes requests on the main thread. */
614   @Override
handleMessage(Message msg)615   public boolean handleMessage(Message msg) {
616     switch (msg.what) {
617       case MESSAGE_REQUEST_LOADING:
618         {
619           mLoadingRequested = false;
620           if (!mPaused) {
621             ensureLoaderThread();
622             mLoaderThread.requestLoading();
623           }
624           return true;
625         }
626 
627       case MESSAGE_PHOTOS_LOADED:
628         {
629           if (!mPaused) {
630             processLoadedImages();
631           }
632           if (DEBUG) {
633             dumpStats();
634           }
635           return true;
636         }
637     }
638     return false;
639   }
640 
ensureLoaderThread()641   public void ensureLoaderThread() {
642     if (mLoaderThread == null) {
643       mLoaderThread = new LoaderThread(mContext.getContentResolver());
644       mLoaderThread.start();
645     }
646   }
647 
648   /**
649    * Goes over pending loading requests and displays loaded photos. If some of the photos still
650    * haven't been loaded, sends another request for image loading.
651    */
processLoadedImages()652   private void processLoadedImages() {
653     final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
654     while (iterator.hasNext()) {
655       final Entry<ImageView, Request> entry = iterator.next();
656       // TODO: Temporarily disable contact photo fading in, until issues with
657       // RoundedBitmapDrawables overlapping the default image drawables are resolved.
658       final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false);
659       if (loaded) {
660         iterator.remove();
661       }
662     }
663 
664     softenCache();
665 
666     if (!mPendingRequests.isEmpty()) {
667       requestLoading();
668     }
669   }
670 
671   /**
672    * Removes strong references to loaded bitmaps to allow them to be garbage collected if needed.
673    * Some of the bitmaps will still be retained by {@link #mBitmapCache}.
674    */
softenCache()675   private void softenCache() {
676     for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
677       holder.bitmap = null;
678     }
679   }
680 
681   /** Stores the supplied bitmap in cache. */
cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent)682   private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
683     if (DEBUG) {
684       BitmapHolder prev = mBitmapHolderCache.get(key);
685       if (prev != null && prev.bytes != null) {
686         LogUtil.d(
687             "ContactPhotoManagerImpl.cacheBitmap",
688             "overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
689         if (prev.fresh) {
690           mFreshCacheOverwrite.incrementAndGet();
691         } else {
692           mStaleCacheOverwrite.incrementAndGet();
693         }
694       }
695       LogUtil.d(
696           "ContactPhotoManagerImpl.cacheBitmap",
697           "caching data: key=" + key + ", " + (bytes == null ? "<null>" : btk(bytes.length)));
698     }
699     BitmapHolder holder =
700         new BitmapHolder(bytes, bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
701 
702     // Unless this image is being preloaded, decode it right away while
703     // we are still on the background thread.
704     if (!preloading) {
705       inflateBitmap(holder, requestedExtent);
706     }
707 
708     if (bytes != null) {
709       mBitmapHolderCache.put(key, holder);
710       if (mBitmapHolderCache.get(key) != holder) {
711         LogUtil.w("ContactPhotoManagerImpl.cacheBitmap", "bitmap too big to fit in cache.");
712         mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
713       }
714     } else {
715       mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
716     }
717 
718     mBitmapHolderCacheAllUnfresh = false;
719   }
720 
721   /**
722    * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
723    * already loaded
724    */
obtainPhotoIdsAndUrisToLoad( Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris)725   private void obtainPhotoIdsAndUrisToLoad(
726       Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris) {
727     photoIds.clear();
728     photoIdsAsStrings.clear();
729     uris.clear();
730 
731     boolean jpegsDecoded = false;
732 
733     /*
734      * Since the call is made from the loader thread, the map could be
735      * changing during the iteration. That's not really a problem:
736      * ConcurrentHashMap will allow those changes to happen without throwing
737      * exceptions. Since we may miss some requests in the situation of
738      * concurrent change, we will need to check the map again once loading
739      * is complete.
740      */
741     Iterator<Request> iterator = mPendingRequests.values().iterator();
742     while (iterator.hasNext()) {
743       Request request = iterator.next();
744       final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
745       if (holder == BITMAP_UNAVAILABLE) {
746         continue;
747       }
748       if (holder != null
749           && holder.bytes != null
750           && holder.fresh
751           && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
752         // This was previously loaded but we don't currently have the inflated Bitmap
753         inflateBitmap(holder, request.getRequestedExtent());
754         jpegsDecoded = true;
755       } else {
756         if (holder == null || !holder.fresh) {
757           if (request.isUriRequest()) {
758             uris.add(request);
759           } else {
760             photoIds.add(request.getId());
761             photoIdsAsStrings.add(String.valueOf(request.mId));
762           }
763         }
764       }
765     }
766 
767     if (jpegsDecoded) {
768       mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
769     }
770   }
771 
772   /** Maintains the state of a particular photo. */
773   private static class BitmapHolder {
774 
775     final byte[] bytes;
776     final int originalSmallerExtent;
777 
778     volatile boolean fresh;
779     Bitmap bitmap;
780     Reference<Bitmap> bitmapRef;
781     int decodedSampleSize;
782 
BitmapHolder(byte[] bytes, int originalSmallerExtent)783     public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
784       this.bytes = bytes;
785       this.fresh = true;
786       this.originalSmallerExtent = originalSmallerExtent;
787     }
788   }
789 
790   /**
791    * A holder for either a Uri or an id and a flag whether this was requested for the dark or light
792    * theme
793    */
794   private static final class Request {
795 
796     private final long mId;
797     private final Uri mUri;
798     private final boolean mDarkTheme;
799     private final int mRequestedExtent;
800     private final DefaultImageProvider mDefaultProvider;
801     /** Whether or not the contact photo is to be displayed as a circle */
802     private final boolean mIsCircular;
803 
Request( long id, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)804     private Request(
805         long id,
806         Uri uri,
807         int requestedExtent,
808         boolean darkTheme,
809         boolean isCircular,
810         DefaultImageProvider defaultProvider) {
811       mId = id;
812       mUri = uri;
813       mDarkTheme = darkTheme;
814       mIsCircular = isCircular;
815       mRequestedExtent = requestedExtent;
816       mDefaultProvider = defaultProvider;
817     }
818 
createFromThumbnailId( long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)819     public static Request createFromThumbnailId(
820         long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) {
821       return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider);
822     }
823 
createFromUri( Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)824     public static Request createFromUri(
825         Uri uri,
826         int requestedExtent,
827         boolean darkTheme,
828         boolean isCircular,
829         DefaultImageProvider defaultProvider) {
830       return new Request(
831           0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, defaultProvider);
832     }
833 
isUriRequest()834     public boolean isUriRequest() {
835       return mUri != null;
836     }
837 
getUri()838     public Uri getUri() {
839       return mUri;
840     }
841 
getId()842     public long getId() {
843       return mId;
844     }
845 
getRequestedExtent()846     public int getRequestedExtent() {
847       return mRequestedExtent;
848     }
849 
850     @Override
hashCode()851     public int hashCode() {
852       final int prime = 31;
853       int result = 1;
854       result = prime * result + (int) (mId ^ (mId >>> 32));
855       result = prime * result + mRequestedExtent;
856       result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
857       return result;
858     }
859 
860     @Override
equals(Object obj)861     public boolean equals(Object obj) {
862       if (this == obj) {
863         return true;
864       }
865       if (obj == null) {
866         return false;
867       }
868       if (getClass() != obj.getClass()) {
869         return false;
870       }
871       final Request that = (Request) obj;
872       if (mId != that.mId) {
873         return false;
874       }
875       if (mRequestedExtent != that.mRequestedExtent) {
876         return false;
877       }
878       if (!UriUtils.areEqual(mUri, that.mUri)) {
879         return false;
880       }
881       // Don't compare equality of mDarkTheme because it is only used in the default contact
882       // photo case. When the contact does have a photo, the contact photo is the same
883       // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
884       // twice.
885       return true;
886     }
887 
getKey()888     public Object getKey() {
889       return mUri == null ? mId : mUri;
890     }
891 
892     /**
893      * Applies the default image to the current view. If the request is URI-based, looks for the
894      * contact type encoded fragment to determine if this is a request for a business photo, in
895      * which case we will load the default business photo.
896      *
897      * @param view The current image view to apply the image to.
898      * @param isCircular Whether the image is circular or not.
899      */
applyDefaultImage(ImageView view, boolean isCircular)900     public void applyDefaultImage(ImageView view, boolean isCircular) {
901       final DefaultImageRequest request;
902 
903       if (isCircular) {
904         request =
905             ContactPhotoManager.isBusinessContactUri(mUri)
906                 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
907                 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
908       } else {
909         request =
910             ContactPhotoManager.isBusinessContactUri(mUri)
911                 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
912                 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
913       }
914       mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
915     }
916   }
917 
918   /** The thread that performs loading of photos from the database. */
919   private class LoaderThread extends HandlerThread implements Callback {
920 
921     private static final int BUFFER_SIZE = 1024 * 16;
922     private static final int MESSAGE_PRELOAD_PHOTOS = 0;
923     private static final int MESSAGE_LOAD_PHOTOS = 1;
924 
925     /** A pause between preload batches that yields to the UI thread. */
926     private static final int PHOTO_PRELOAD_DELAY = 1000;
927 
928     /** Number of photos to preload per batch. */
929     private static final int PRELOAD_BATCH = 25;
930 
931     /**
932      * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size
933      * of a photo is 4kb, then this number should be 2Mb/4kb = 500.
934      */
935     private static final int MAX_PHOTOS_TO_PRELOAD = 100;
936 
937     private static final int PRELOAD_STATUS_NOT_STARTED = 0;
938     private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
939     private static final int PRELOAD_STATUS_DONE = 2;
940     private final ContentResolver mResolver;
941     private final StringBuilder mStringBuilder = new StringBuilder();
942     private final Set<Long> mPhotoIds = new HashSet<>();
943     private final Set<String> mPhotoIdsAsStrings = new HashSet<>();
944     private final Set<Request> mPhotoUris = new HashSet<>();
945     private final List<Long> mPreloadPhotoIds = new ArrayList<>();
946     private Handler mLoaderThreadHandler;
947     private byte[] mBuffer;
948     private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
949 
LoaderThread(ContentResolver resolver)950     public LoaderThread(ContentResolver resolver) {
951       super(LOADER_THREAD_NAME);
952       mResolver = resolver;
953     }
954 
ensureHandler()955     public void ensureHandler() {
956       if (mLoaderThreadHandler == null) {
957         mLoaderThreadHandler = new Handler(getLooper(), this);
958       }
959     }
960 
961     /**
962      * Kicks off preloading of the next batch of photos on the background thread. Preloading will
963      * happen after a delay: we want to yield to the UI thread as much as possible.
964      *
965      * <p>If preloading is already complete, does nothing.
966      */
requestPreloading()967     public void requestPreloading() {
968       if (mPreloadStatus == PRELOAD_STATUS_DONE) {
969         return;
970       }
971 
972       ensureHandler();
973       if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
974         return;
975       }
976 
977       mLoaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
978     }
979 
980     /**
981      * Sends a message to this thread to load requested photos. Cancels a preloading request, if
982      * any: we don't want preloading to impede loading of the photos we need to display now.
983      */
requestLoading()984     public void requestLoading() {
985       ensureHandler();
986       mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
987       mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
988     }
989 
990     /**
991      * Receives the above message, loads photos and then sends a message to the main thread to
992      * process them.
993      */
994     @Override
handleMessage(Message msg)995     public boolean handleMessage(Message msg) {
996       switch (msg.what) {
997         case MESSAGE_PRELOAD_PHOTOS:
998           preloadPhotosInBackground();
999           break;
1000         case MESSAGE_LOAD_PHOTOS:
1001           loadPhotosInBackground();
1002           break;
1003       }
1004       return true;
1005     }
1006 
1007     /**
1008      * The first time it is called, figures out which photos need to be preloaded. Each subsequent
1009      * call preloads the next batch of photos and requests another cycle of preloading after a
1010      * delay. The whole process ends when we either run out of photos to preload or fill up cache.
1011      */
1012     @WorkerThread
preloadPhotosInBackground()1013     private void preloadPhotosInBackground() {
1014       if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
1015         return;
1016       }
1017 
1018       if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1019         return;
1020       }
1021 
1022       if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
1023         queryPhotosForPreload();
1024         if (mPreloadPhotoIds.isEmpty()) {
1025           mPreloadStatus = PRELOAD_STATUS_DONE;
1026         } else {
1027           mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
1028         }
1029         requestPreloading();
1030         return;
1031       }
1032 
1033       if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
1034         mPreloadStatus = PRELOAD_STATUS_DONE;
1035         return;
1036       }
1037 
1038       mPhotoIds.clear();
1039       mPhotoIdsAsStrings.clear();
1040 
1041       int count = 0;
1042       int preloadSize = mPreloadPhotoIds.size();
1043       while (preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
1044         preloadSize--;
1045         count++;
1046         Long photoId = mPreloadPhotoIds.get(preloadSize);
1047         mPhotoIds.add(photoId);
1048         mPhotoIdsAsStrings.add(photoId.toString());
1049         mPreloadPhotoIds.remove(preloadSize);
1050       }
1051 
1052       loadThumbnails(true);
1053 
1054       if (preloadSize == 0) {
1055         mPreloadStatus = PRELOAD_STATUS_DONE;
1056       }
1057 
1058       LogUtil.v(
1059           "ContactPhotoManagerImpl.preloadPhotosInBackground",
1060           "preloaded " + count + " photos.  cached bytes: " + mBitmapHolderCache.size());
1061 
1062       requestPreloading();
1063     }
1064 
1065     @WorkerThread
queryPhotosForPreload()1066     private void queryPhotosForPreload() {
1067       Cursor cursor = null;
1068       try {
1069         Uri uri =
1070             Contacts.CONTENT_URI
1071                 .buildUpon()
1072                 .appendQueryParameter(
1073                     ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
1074                 .appendQueryParameter(
1075                     ContactsContract.LIMIT_PARAM_KEY, String.valueOf(MAX_PHOTOS_TO_PRELOAD))
1076                 .build();
1077         cursor =
1078             mResolver.query(
1079                 uri,
1080                 new String[] {Contacts.PHOTO_ID},
1081                 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
1082                 null,
1083                 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
1084 
1085         if (cursor != null) {
1086           while (cursor.moveToNext()) {
1087             // Insert them in reverse order, because we will be taking
1088             // them from the end of the list for loading.
1089             mPreloadPhotoIds.add(0, cursor.getLong(0));
1090           }
1091         }
1092       } finally {
1093         if (cursor != null) {
1094           cursor.close();
1095         }
1096       }
1097     }
1098 
1099     @WorkerThread
loadPhotosInBackground()1100     private void loadPhotosInBackground() {
1101       if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
1102         return;
1103       }
1104       obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
1105       loadThumbnails(false);
1106       loadUriBasedPhotos();
1107       requestPreloading();
1108     }
1109 
1110     /** Loads thumbnail photos with ids */
1111     @WorkerThread
loadThumbnails(boolean preloading)1112     private void loadThumbnails(boolean preloading) {
1113       if (mPhotoIds.isEmpty()) {
1114         return;
1115       }
1116 
1117       // Remove loaded photos from the preload queue: we don't want
1118       // the preloading process to load them again.
1119       if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
1120         for (Long id : mPhotoIds) {
1121           mPreloadPhotoIds.remove(id);
1122         }
1123         if (mPreloadPhotoIds.isEmpty()) {
1124           mPreloadStatus = PRELOAD_STATUS_DONE;
1125         }
1126       }
1127 
1128       mStringBuilder.setLength(0);
1129       mStringBuilder.append(Photo._ID + " IN(");
1130       for (int i = 0; i < mPhotoIds.size(); i++) {
1131         if (i != 0) {
1132           mStringBuilder.append(',');
1133         }
1134         mStringBuilder.append('?');
1135       }
1136       mStringBuilder.append(')');
1137 
1138       Cursor cursor = null;
1139       try {
1140         if (DEBUG) {
1141           LogUtil.d(
1142               "ContactPhotoManagerImpl.loadThumbnails",
1143               "loading " + TextUtils.join(",", mPhotoIdsAsStrings));
1144         }
1145         cursor =
1146             mResolver.query(
1147                 Data.CONTENT_URI,
1148                 COLUMNS,
1149                 mStringBuilder.toString(),
1150                 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
1151                 null);
1152 
1153         if (cursor != null) {
1154           while (cursor.moveToNext()) {
1155             Long id = cursor.getLong(0);
1156             byte[] bytes = cursor.getBlob(1);
1157             cacheBitmap(id, bytes, preloading, -1);
1158             mPhotoIds.remove(id);
1159           }
1160         }
1161       } finally {
1162         if (cursor != null) {
1163           cursor.close();
1164         }
1165       }
1166 
1167       // Remaining photos were not found in the contacts database (but might be in profile).
1168       for (Long id : mPhotoIds) {
1169         if (ContactsContract.isProfileId(id)) {
1170           Cursor profileCursor = null;
1171           try {
1172             profileCursor =
1173                 mResolver.query(
1174                     ContentUris.withAppendedId(Data.CONTENT_URI, id), COLUMNS, null, null, null);
1175             if (profileCursor != null && profileCursor.moveToFirst()) {
1176               cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), preloading, -1);
1177             } else {
1178               // Couldn't load a photo this way either.
1179               cacheBitmap(id, null, preloading, -1);
1180             }
1181           } finally {
1182             if (profileCursor != null) {
1183               profileCursor.close();
1184             }
1185           }
1186         } else {
1187           // Not a profile photo and not found - mark the cache accordingly
1188           cacheBitmap(id, null, preloading, -1);
1189         }
1190       }
1191 
1192       mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1193     }
1194 
1195     /**
1196      * Loads photos referenced with Uris. Those can be remote thumbnails (from directory searches),
1197      * display photos etc
1198      */
1199     @WorkerThread
loadUriBasedPhotos()1200     private void loadUriBasedPhotos() {
1201       for (Request uriRequest : mPhotoUris) {
1202         // Keep the original URI and use this to key into the cache.  Failure to do so will
1203         // result in an image being continually reloaded into cache if the original URI
1204         // has a contact type encodedFragment (eg nearby places business photo URLs).
1205         Uri originalUri = uriRequest.getUri();
1206 
1207         // Strip off the "contact type" we added to the URI to ensure it was identifiable as
1208         // a business photo -- there is no need to pass this on to the server.
1209         Uri uri = ContactPhotoManager.removeContactType(originalUri);
1210 
1211         if (mBuffer == null) {
1212           mBuffer = new byte[BUFFER_SIZE];
1213         }
1214         try {
1215           if (DEBUG) {
1216             LogUtil.d("ContactPhotoManagerImpl.loadUriBasedPhotos", "loading " + uri);
1217           }
1218           final String scheme = uri.getScheme();
1219           InputStream is = null;
1220           if (scheme.equals("http") || scheme.equals("https")) {
1221             TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
1222             final HttpURLConnection connection =
1223                 (HttpURLConnection) new URL(uri.toString()).openConnection();
1224 
1225             // Include the user agent if it is specified.
1226             if (!TextUtils.isEmpty(mUserAgent)) {
1227               connection.setRequestProperty("User-Agent", mUserAgent);
1228             }
1229             try {
1230               is = connection.getInputStream();
1231             } catch (IOException e) {
1232               connection.disconnect();
1233               is = null;
1234             }
1235             TrafficStats.clearThreadStatsTag();
1236           } else {
1237             is = mResolver.openInputStream(uri);
1238           }
1239           if (is != null) {
1240             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1241             try {
1242               int size;
1243               while ((size = is.read(mBuffer)) != -1) {
1244                 baos.write(mBuffer, 0, size);
1245               }
1246             } finally {
1247               is.close();
1248             }
1249             cacheBitmap(originalUri, baos.toByteArray(), false, uriRequest.getRequestedExtent());
1250             mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1251           } else {
1252             LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri);
1253             cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
1254           }
1255         } catch (final Exception | OutOfMemoryError ex) {
1256           LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri, ex);
1257           cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
1258         }
1259       }
1260     }
1261   }
1262 }
1263