Icon.java revision b9f7aac3488873677377b36c57338d758098f78e
1/* 2 * Copyright (C) 2015 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 17package android.graphics.drawable; 18 19import android.annotation.DrawableRes; 20import android.content.ContentResolver; 21import android.content.pm.PackageManager; 22import android.content.res.Resources; 23import android.content.Context; 24import android.graphics.Bitmap; 25import android.graphics.BitmapFactory; 26import android.net.Uri; 27import android.os.AsyncTask; 28import android.os.Handler; 29import android.os.Message; 30import android.os.Parcel; 31import android.os.Parcelable; 32import android.util.Log; 33 34import java.io.ByteArrayOutputStream; 35import java.io.File; 36import java.io.FileInputStream; 37import java.io.FileNotFoundException; 38import java.io.InputStream; 39import java.lang.IllegalArgumentException; 40import java.lang.Override; 41 42/** 43 * An umbrella container for several serializable graphics representations, including Bitmaps, 44 * compressed bitmap images (e.g. JPG or PNG), and drawable resources (including vectors). 45 * 46 * <a href="https://developer.android.com/training/displaying-bitmaps/index.html">Much ink</a> 47 * has been spilled on the best way to load images, and many clients may have different needs when 48 * it comes to threading and fetching. This class is therefore focused on encapsulation rather than 49 * behavior. 50 */ 51 52public final class Icon implements Parcelable { 53 private static final String TAG = "Icon"; 54 55 private static final int TYPE_BITMAP = 1; 56 private static final int TYPE_RESOURCE = 2; 57 private static final int TYPE_DATA = 3; 58 private static final int TYPE_URI = 4; 59 60 private final int mType; 61 62 // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed 63 // based on the value of mType. 64 65 // TYPE_BITMAP: Bitmap 66 // TYPE_RESOURCE: Resources 67 // TYPE_DATA: DataBytes 68 private Object mObj1; 69 70 // TYPE_RESOURCE: package name 71 // TYPE_URI: uri string 72 private String mString1; 73 74 // TYPE_RESOURCE: resId 75 // TYPE_DATA: data length 76 private int mInt1; 77 78 // TYPE_DATA: data offset 79 private int mInt2; 80 81 // Internal accessors for different mType variants 82 private Bitmap getBitmap() { 83 if (mType != TYPE_BITMAP) { 84 throw new IllegalStateException("called getBitmap() on " + this); 85 } 86 return (Bitmap) mObj1; 87 } 88 89 private int getDataLength() { 90 if (mType != TYPE_DATA) { 91 throw new IllegalStateException("called getDataLength() on " + this); 92 } 93 synchronized (this) { 94 return mInt1; 95 } 96 } 97 98 private int getDataOffset() { 99 if (mType != TYPE_DATA) { 100 throw new IllegalStateException("called getDataOffset() on " + this); 101 } 102 synchronized (this) { 103 return mInt2; 104 } 105 } 106 107 private byte[] getDataBytes() { 108 if (mType != TYPE_DATA) { 109 throw new IllegalStateException("called getDataBytes() on " + this); 110 } 111 synchronized (this) { 112 return (byte[]) mObj1; 113 } 114 } 115 116 private Resources getResources() { 117 if (mType != TYPE_RESOURCE) { 118 throw new IllegalStateException("called getResources() on " + this); 119 } 120 return (Resources) mObj1; 121 } 122 123 private String getResPackage() { 124 if (mType != TYPE_RESOURCE) { 125 throw new IllegalStateException("called getResPackage() on " + this); 126 } 127 return mString1; 128 } 129 130 private int getResId() { 131 if (mType != TYPE_RESOURCE) { 132 throw new IllegalStateException("called getResId() on " + this); 133 } 134 return mInt1; 135 } 136 137 private String getUriString() { 138 if (mType != TYPE_URI) { 139 throw new IllegalStateException("called getUriString() on " + this); 140 } 141 return mString1; 142 } 143 144 private Uri getUri() { 145 return Uri.parse(getUriString()); 146 } 147 148 // Convert a int32 into a four-char string 149 private static final String typeToString(int x) { 150 switch (x) { 151 case TYPE_BITMAP: return "BITMAP"; 152 case TYPE_DATA: return "DATA"; 153 case TYPE_RESOURCE: return "RESOURCE"; 154 case TYPE_URI: return "URI"; 155 default: return "UNKNOWN"; 156 } 157 } 158 159 /** 160 * Invokes {@link #loadDrawable(Context)} on the given {@link android.os.Handler Handler} 161 * and then sends <code>andThen</code> to the same Handler when finished. 162 * 163 * @param context {@link android.content.Context Context} in which to load the drawable; see 164 * {@link #loadDrawable(Context)} 165 * @param andThen {@link android.os.Message} to send to its target once the drawable 166 * is available. The {@link android.os.Message#obj obj} 167 * property is populated with the Drawable. 168 */ 169 public void loadDrawableAsync(Context context, Message andThen) { 170 if (andThen.getTarget() == null) { 171 throw new IllegalArgumentException("callback message must have a target handler"); 172 } 173 new LoadDrawableTask(context, andThen).runAsync(); 174 } 175 176 /** 177 * Invokes {@link #loadDrawable(Context)} on a background thread 178 * and then runs <code>andThen</code> on the UI thread when finished. 179 * 180 * @param context {@link android.content.Context Context} in which to load the drawable; see 181 * {@link #loadDrawable(Context)} 182 * @param handler {@link android.os.Handler} on which to run <code>andThen</code>. 183 * @param listener a callback to run on the provided 184 * Handler once the drawable is available. 185 */ 186 public void loadDrawableAsync(Context context, Handler handler, 187 final OnDrawableLoadedListener listener) { 188 new LoadDrawableTask(context, handler, listener).runAsync(); 189 } 190 191 /** 192 * Returns a Drawable that can be used to draw the image inside this Icon, constructing it 193 * if necessary. Depending on the type of image, this may not be something you want to do on 194 * the UI thread, so consider using 195 * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead. 196 * 197 * @param context {@link android.content.Context Context} in which to load the drawable; used 198 * to access {@link android.content.res.Resources Resources}, for example. 199 * @return A fresh instance of a drawable for this image, yours to keep. 200 */ 201 public Drawable loadDrawable(Context context) { 202 switch (mType) { 203 case TYPE_BITMAP: 204 return new BitmapDrawable(context.getResources(), getBitmap()); 205 case TYPE_RESOURCE: 206 if (getResources() == null) { 207 if (getResPackage() == null || "android".equals(getResPackage())) { 208 mObj1 = Resources.getSystem(); 209 } else { 210 final PackageManager pm = context.getPackageManager(); 211 try { 212 mObj1 = pm.getResourcesForApplication(getResPackage()); 213 } catch (PackageManager.NameNotFoundException e) { 214 Log.e(TAG, 215 String.format("Unable to find package '%s'", getResPackage()), 216 e); 217 break; 218 } 219 } 220 } 221 return getResources().getDrawable(getResId(), context.getTheme()); 222 case TYPE_DATA: 223 return new BitmapDrawable(context.getResources(), 224 BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) 225 ); 226 case TYPE_URI: 227 final Uri uri = getUri(); 228 final String scheme = uri.getScheme(); 229 InputStream is = null; 230 if (ContentResolver.SCHEME_CONTENT.equals(scheme) 231 || ContentResolver.SCHEME_FILE.equals(scheme)) { 232 try { 233 is = context.getContentResolver().openInputStream(uri); 234 } catch (Exception e) { 235 Log.w(TAG, "Unable to load image from URI: " + uri, e); 236 } 237 } else { 238 try { 239 is = new FileInputStream(new File(mString1)); 240 } catch (FileNotFoundException e) { 241 Log.w(TAG, "Unable to load image from path: " + uri, e); 242 } 243 } 244 if (is != null) { 245 return new BitmapDrawable(context.getResources(), 246 BitmapFactory.decodeStream(is)); 247 } 248 break; 249 } 250 return null; 251 } 252 253 private Icon(int mType) { 254 this.mType = mType; 255 } 256 257 /** 258 * Create a Icon pointing to a drawable resource. 259 * @param res Resources for a package containing the resource in question 260 * @param resid ID of the drawable resource 261 */ 262 public static Icon createWithResource(Resources res, @DrawableRes int resid) { 263 final Icon rep = new Icon(TYPE_RESOURCE); 264 rep.mObj1 = res; 265 rep.mInt1 = resid; 266 rep.mString1 = res.getResourcePackageName(resid); 267 return rep; 268 } 269 270 /** 271 * Create a Icon pointing to a bitmap in memory. 272 * @param bits A valid {@link android.graphics.Bitmap} object 273 */ 274 public static Icon createWithBitmap(Bitmap bits) { 275 final Icon rep = new Icon(TYPE_BITMAP); 276 rep.mObj1 = bits; 277 return rep; 278 } 279 280 /** 281 * Create a Icon pointing to a compressed bitmap stored in a byte array. 282 * @param data Byte array storing compressed bitmap data of a type that 283 * {@link android.graphics.BitmapFactory} 284 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 285 * @param offset Offset into <code>data</code> at which the bitmap data starts 286 * @param length Length of the bitmap data 287 */ 288 public static Icon createWithData(byte[] data, int offset, int length) { 289 final Icon rep = new Icon(TYPE_DATA); 290 rep.mObj1 = data; 291 rep.mInt1 = length; 292 rep.mInt2 = offset; 293 return rep; 294 } 295 296 /** 297 * Create a Icon pointing to a content specified by URI. 298 * 299 * @param uri A uri referring to local content:// or file:// image data. 300 */ 301 public static Icon createWithContentUri(String uri) { 302 final Icon rep = new Icon(TYPE_URI); 303 rep.mString1 = uri; 304 return rep; 305 } 306 307 /** 308 * Create a Icon pointing to a content specified by URI. 309 * 310 * @param uri A uri referring to local content:// or file:// image data. 311 */ 312 public static Icon createWithContentUri(Uri uri) { 313 final Icon rep = new Icon(TYPE_URI); 314 rep.mString1 = uri.toString(); 315 return rep; 316 } 317 318 /** 319 * Create a Icon pointing to 320 * 321 * @param path A path to a file that contains compressed bitmap data of 322 * a type that {@link android.graphics.BitmapFactory} can decode. 323 */ 324 public static Icon createWithFilePath(String path) { 325 final Icon rep = new Icon(TYPE_URI); 326 rep.mString1 = path; 327 return rep; 328 } 329 330 @Override 331 public String toString() { 332 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 333 switch (mType) { 334 case TYPE_BITMAP: 335 sb.append(" size=") 336 .append(getBitmap().getWidth()) 337 .append("x") 338 .append(getBitmap().getHeight()); 339 break; 340 case TYPE_RESOURCE: 341 sb.append(" pkg=") 342 .append(getResPackage()) 343 .append(" id=") 344 .append(String.format("%08x", getResId())); 345 break; 346 case TYPE_DATA: 347 sb.append(" len=").append(getDataLength()); 348 if (getDataOffset() != 0) { 349 sb.append(" off=").append(getDataOffset()); 350 } 351 break; 352 case TYPE_URI: 353 sb.append(" uri=").append(getUriString()); 354 break; 355 } 356 sb.append(")"); 357 return sb.toString(); 358 } 359 360 /** 361 * Parcelable interface 362 */ 363 public int describeContents() { 364 return (mType == TYPE_BITMAP || mType == TYPE_DATA) 365 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 366 } 367 368 // ===== Parcelable interface ====== 369 370 private Icon(Parcel in) { 371 this(in.readInt()); 372 switch (mType) { 373 case TYPE_BITMAP: 374 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 375 mObj1 = bits; 376 break; 377 case TYPE_RESOURCE: 378 final String pkg = in.readString(); 379 final int resId = in.readInt(); 380 mString1 = pkg; 381 mInt1 = resId; 382 break; 383 case TYPE_DATA: 384 final int len = in.readInt(); 385 final byte[] a = in.readBlob(); 386 if (len != a.length) { 387 throw new RuntimeException("internal unparceling error: blob length (" 388 + a.length + ") != expected length (" + len + ")"); 389 } 390 mInt1 = len; 391 mObj1 = a; 392 break; 393 case TYPE_URI: 394 final String uri = in.readString(); 395 mString1 = uri; 396 break; 397 default: 398 throw new RuntimeException("invalid " 399 + this.getClass().getSimpleName() + " type in parcel: " + mType); 400 } 401 } 402 403 @Override 404 public void writeToParcel(Parcel dest, int flags) { 405 switch (mType) { 406 case TYPE_BITMAP: 407 final Bitmap bits = getBitmap(); 408 dest.writeInt(TYPE_BITMAP); 409 getBitmap().writeToParcel(dest, flags); 410 break; 411 case TYPE_RESOURCE: 412 dest.writeInt(TYPE_RESOURCE); 413 dest.writeString(getResPackage()); 414 dest.writeInt(getResId()); 415 break; 416 case TYPE_DATA: 417 dest.writeInt(TYPE_DATA); 418 dest.writeInt(getDataLength()); 419 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 420 break; 421 case TYPE_URI: 422 dest.writeInt(TYPE_URI); 423 dest.writeString(getUriString()); 424 break; 425 } 426 } 427 428 public static final Parcelable.Creator<Icon> CREATOR 429 = new Parcelable.Creator<Icon>() { 430 public Icon createFromParcel(Parcel in) { 431 return new Icon(in); 432 } 433 434 public Icon[] newArray(int size) { 435 return new Icon[size]; 436 } 437 }; 438 439 /** 440 * Implement this interface to receive notification when 441 * {@link #loadDrawableAsync(Context, Handler, OnDrawableLoadedListener) loadDrawableAsync} 442 * is finished and your Drawable is ready. 443 */ 444 public interface OnDrawableLoadedListener { 445 void onDrawableLoaded(Drawable d); 446 } 447 448 /** 449 * Wrapper around loadDrawable that does its work on a pooled thread and then 450 * fires back the given (targeted) Message. 451 */ 452 private class LoadDrawableTask implements Runnable { 453 final Context mContext; 454 final Message mMessage; 455 456 public LoadDrawableTask(Context context, final Handler handler, 457 final OnDrawableLoadedListener listener) { 458 mContext = context; 459 mMessage = Message.obtain(handler, new Runnable() { 460 @Override 461 public void run() { 462 listener.onDrawableLoaded((Drawable) mMessage.obj); 463 } 464 }); 465 } 466 467 public LoadDrawableTask(Context context, Message message) { 468 mContext = context; 469 mMessage = message; 470 } 471 472 @Override 473 public void run() { 474 mMessage.obj = loadDrawable(mContext); 475 mMessage.sendToTarget(); 476 } 477 478 public void runAsync() { 479 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 480 } 481 } 482} 483