1 /* 2 * Copyright (C) 2012 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.mail.photomanager; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.text.TextUtils; 22 import android.util.LruCache; 23 24 import com.android.mail.ContactInfo; 25 import com.android.mail.SenderInfoLoader; 26 import com.android.mail.ui.ImageCanvas; 27 import com.android.mail.utils.LogUtils; 28 import com.google.common.base.Objects; 29 import com.google.common.collect.ImmutableMap; 30 31 import java.util.Collection; 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.Map; 35 import java.util.Set; 36 37 /** 38 * Asynchronously loads contact photos and maintains a cache of photos. 39 */ 40 public class ContactPhotoManager extends PhotoManager { 41 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos"; 42 43 /** 44 * An LRU cache for photo ids mapped to contact addresses. 45 */ 46 private final LruCache<String, Long> mPhotoIdCache; 47 private final LetterTileProvider mLetterTileProvider; 48 49 /** Cache size for {@link #mPhotoIdCache}. Starting with 500 entries. */ 50 private static final int PHOTO_ID_CACHE_SIZE = 500; 51 52 /** 53 * Requests the singleton instance with data bound from the available authenticators. This 54 * method can safely be called from the UI thread. 55 */ 56 public static ContactPhotoManager getInstance(Context context) { 57 Context applicationContext = context.getApplicationContext(); 58 ContactPhotoManager service = 59 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE); 60 if (service == null) { 61 service = createContactPhotoManager(applicationContext); 62 LogUtils.e(TAG, "No contact photo service in context: " + applicationContext); 63 } 64 return service; 65 } 66 67 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { 68 return new ContactPhotoManager(context); 69 } 70 71 public static int generateHash(ImageCanvas view, int pos, Object key) { 72 return Objects.hashCode(view, pos, key); 73 } 74 75 private ContactPhotoManager(Context context) { 76 super(context); 77 mPhotoIdCache = new LruCache<String, Long>(PHOTO_ID_CACHE_SIZE); 78 mLetterTileProvider = new LetterTileProvider(context); 79 } 80 81 @Override 82 protected DefaultImageProvider getDefaultImageProvider() { 83 return mLetterTileProvider; 84 } 85 86 @Override 87 protected int getHash(PhotoIdentifier id, ImageCanvas view) { 88 final ContactIdentifier contactId = (ContactIdentifier) id; 89 return generateHash(view, contactId.pos, contactId.getKey()); 90 } 91 92 @Override 93 protected PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) { 94 return new ContactPhotoLoaderThread(contentResolver); 95 } 96 97 @Override 98 public void clear() { 99 super.clear(); 100 mPhotoIdCache.evictAll(); 101 } 102 103 public static class ContactIdentifier extends PhotoIdentifier { 104 public final String name; 105 public final String emailAddress; 106 public final int pos; 107 108 public ContactIdentifier(String name, String emailAddress, int pos) { 109 this.name = name; 110 this.emailAddress = emailAddress; 111 this.pos = pos; 112 } 113 114 @Override 115 public boolean isValid() { 116 return !TextUtils.isEmpty(emailAddress); 117 } 118 119 @Override 120 public Object getKey() { 121 return emailAddress; 122 } 123 124 @Override 125 public int hashCode() { 126 int hash = 17; 127 hash = 31 * hash + (emailAddress != null ? emailAddress.hashCode() : 0); 128 hash = 31 * hash + (name != null ? name.hashCode() : 0); 129 hash = 31 * hash + pos; 130 return hash; 131 } 132 133 @Override 134 public boolean equals(Object obj) { 135 if (this == obj) 136 return true; 137 if (obj == null) 138 return false; 139 if (getClass() != obj.getClass()) 140 return false; 141 ContactIdentifier other = (ContactIdentifier) obj; 142 return Objects.equal(emailAddress, other.emailAddress) 143 && Objects.equal(name, other.name) && Objects.equal(pos, other.pos); 144 } 145 146 @Override 147 public String toString() { 148 final StringBuilder sb = new StringBuilder("{"); 149 sb.append(super.toString()); 150 sb.append(" name="); 151 sb.append(name); 152 sb.append(" email="); 153 sb.append(emailAddress); 154 sb.append(" pos="); 155 sb.append(pos); 156 sb.append("}"); 157 return sb.toString(); 158 } 159 160 @Override 161 public int compareTo(PhotoIdentifier another) { 162 return 0; 163 } 164 } 165 166 public class ContactPhotoLoaderThread extends PhotoLoaderThread { 167 public ContactPhotoLoaderThread(ContentResolver resolver) { 168 super(resolver); 169 } 170 171 @Override 172 protected Map<String, BitmapHolder> loadPhotos(Collection<Request> requests) { 173 Map<String, BitmapHolder> photos = new HashMap<String, BitmapHolder>(requests.size()); 174 175 Set<String> addresses = new HashSet<String>(); 176 Set<Long> photoIds = new HashSet<Long>(); 177 HashMap<Long, String> photoIdMap = new HashMap<Long, String>(); 178 179 Long match; 180 String emailAddress; 181 for (Request request : requests) { 182 emailAddress = (String) request.getKey(); 183 match = mPhotoIdCache.get(emailAddress); 184 if (match != null) { 185 photoIds.add(match); 186 photoIdMap.put(match, emailAddress); 187 } else { 188 addresses.add(emailAddress); 189 } 190 } 191 192 // get the Map of email addresses to ContactInfo 193 ImmutableMap<String, ContactInfo> emailAddressToContactInfoMap = 194 SenderInfoLoader.loadContactPhotos( 195 getResolver(), addresses, false /* decodeBitmaps */); 196 197 // Put all entries into photos map: a mapping of email addresses to photoBytes. 198 // If there is no ContactInfo, it means we couldn't get a photo for this 199 // address so just put null in for the bytes so that the crazy caching 200 // works properly and we don't get an infinite loop of GC churn. 201 if (emailAddressToContactInfoMap != null) { 202 for (final String address : addresses) { 203 final ContactInfo info = emailAddressToContactInfoMap.get(address); 204 photos.put(address, 205 new BitmapHolder(info != null ? info.photoBytes : null, -1, -1)); 206 } 207 } else { 208 // Still need to set a null result for all addresses, otherwise we end 209 // up in the loop where photo manager attempts to load these again. 210 for (final String address: addresses) { 211 photos.put(address, new BitmapHolder(null, -1, -1)); 212 } 213 } 214 215 return photos; 216 } 217 } 218 } 219