Icon.java revision 08a04c15245c970856022d0779aa27d4d63cdee3
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.Context; 22import android.content.pm.PackageManager; 23import android.content.res.Resources; 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.DataInputStream; 35import java.io.DataOutputStream; 36import java.io.File; 37import java.io.FileInputStream; 38import java.io.FileNotFoundException; 39import java.io.IOException; 40import java.io.InputStream; 41import java.io.OutputStream; 42 43/** 44 * An umbrella container for several serializable graphics representations, including Bitmaps, 45 * compressed bitmap images (e.g. JPG or PNG), and drawable resources (including vectors). 46 * 47 * <a href="https://developer.android.com/training/displaying-bitmaps/index.html">Much ink</a> 48 * has been spilled on the best way to load images, and many clients may have different needs when 49 * it comes to threading and fetching. This class is therefore focused on encapsulation rather than 50 * behavior. 51 */ 52 53public final class Icon implements Parcelable { 54 private static final String TAG = "Icon"; 55 56 /** @hide */ 57 public static final int TYPE_BITMAP = 1; 58 /** @hide */ 59 public static final int TYPE_RESOURCE = 2; 60 /** @hide */ 61 public static final int TYPE_DATA = 3; 62 /** @hide */ 63 public static final int TYPE_URI = 4; 64 65 private static final int VERSION_STREAM_SERIALIZER = 1; 66 67 private final int mType; 68 69 // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed 70 // based on the value of mType. 71 72 // TYPE_BITMAP: Bitmap 73 // TYPE_RESOURCE: Resources 74 // TYPE_DATA: DataBytes 75 private Object mObj1; 76 77 // TYPE_RESOURCE: package name 78 // TYPE_URI: uri string 79 private String mString1; 80 81 // TYPE_RESOURCE: resId 82 // TYPE_DATA: data length 83 private int mInt1; 84 85 // TYPE_DATA: data offset 86 private int mInt2; 87 88 /** 89 * @return The type of image data held in this Icon. One of 90 * {@link #TYPE_BITMAP}, 91 * {@link #TYPE_RESOURCE}, 92 * {@link #TYPE_DATA}, or 93 * {@link #TYPE_URI}. 94 * @hide 95 */ 96 public int getType() { 97 return mType; 98 } 99 100 /** 101 * @return The {@link android.graphics.Bitmap} held by this {@link #TYPE_BITMAP} Icon. 102 * @hide 103 */ 104 public Bitmap getBitmap() { 105 if (mType != TYPE_BITMAP) { 106 throw new IllegalStateException("called getBitmap() on " + this); 107 } 108 return (Bitmap) mObj1; 109 } 110 111 /** 112 * @return The length of the compressed bitmap byte array held by this {@link #TYPE_DATA} Icon. 113 * @hide 114 */ 115 public int getDataLength() { 116 if (mType != TYPE_DATA) { 117 throw new IllegalStateException("called getDataLength() on " + this); 118 } 119 synchronized (this) { 120 return mInt1; 121 } 122 } 123 124 /** 125 * @return The offset into the byte array held by this {@link #TYPE_DATA} Icon at which 126 * valid compressed bitmap data is found. 127 * @hide 128 */ 129 public int getDataOffset() { 130 if (mType != TYPE_DATA) { 131 throw new IllegalStateException("called getDataOffset() on " + this); 132 } 133 synchronized (this) { 134 return mInt2; 135 } 136 } 137 138 /** 139 * @return The byte array held by this {@link #TYPE_DATA} Icon ctonaining compressed 140 * bitmap data. 141 * @hide 142 */ 143 public byte[] getDataBytes() { 144 if (mType != TYPE_DATA) { 145 throw new IllegalStateException("called getDataBytes() on " + this); 146 } 147 synchronized (this) { 148 return (byte[]) mObj1; 149 } 150 } 151 152 /** 153 * @return The {@link android.content.res.Resources} for this {@link #TYPE_RESOURCE} Icon. 154 * @hide 155 */ 156 public Resources getResources() { 157 if (mType != TYPE_RESOURCE) { 158 throw new IllegalStateException("called getResources() on " + this); 159 } 160 return (Resources) mObj1; 161 } 162 163 /** 164 * @return The package containing resources for this {@link #TYPE_RESOURCE} Icon. 165 * @hide 166 */ 167 public String getResPackage() { 168 if (mType != TYPE_RESOURCE) { 169 throw new IllegalStateException("called getResPackage() on " + this); 170 } 171 return mString1; 172 } 173 174 /** 175 * @return The resource ID for this {@link #TYPE_RESOURCE} Icon. 176 * @hide 177 */ 178 public int getResId() { 179 if (mType != TYPE_RESOURCE) { 180 throw new IllegalStateException("called getResId() on " + this); 181 } 182 return mInt1; 183 } 184 185 /** 186 * @return The URI (as a String) for this {@link #TYPE_URI} Icon. 187 * @hide 188 */ 189 public String getUriString() { 190 if (mType != TYPE_URI) { 191 throw new IllegalStateException("called getUriString() on " + this); 192 } 193 return mString1; 194 } 195 196 /** 197 * @return The {@link android.net.Uri} for this {@link #TYPE_URI} Icon. 198 * @hide 199 */ 200 public Uri getUri() { 201 return Uri.parse(getUriString()); 202 } 203 204 private static final String typeToString(int x) { 205 switch (x) { 206 case TYPE_BITMAP: return "BITMAP"; 207 case TYPE_DATA: return "DATA"; 208 case TYPE_RESOURCE: return "RESOURCE"; 209 case TYPE_URI: return "URI"; 210 default: return "UNKNOWN"; 211 } 212 } 213 214 /** 215 * Invokes {@link #loadDrawable(Context)} on the given {@link android.os.Handler Handler} 216 * and then sends <code>andThen</code> to the same Handler when finished. 217 * 218 * @param context {@link android.content.Context Context} in which to load the drawable; see 219 * {@link #loadDrawable(Context)} 220 * @param andThen {@link android.os.Message} to send to its target once the drawable 221 * is available. The {@link android.os.Message#obj obj} 222 * property is populated with the Drawable. 223 */ 224 public void loadDrawableAsync(Context context, Message andThen) { 225 if (andThen.getTarget() == null) { 226 throw new IllegalArgumentException("callback message must have a target handler"); 227 } 228 new LoadDrawableTask(context, andThen).runAsync(); 229 } 230 231 /** 232 * Invokes {@link #loadDrawable(Context)} on a background thread 233 * and then runs <code>andThen</code> on the UI thread when finished. 234 * 235 * @param context {@link Context Context} in which to load the drawable; see 236 * {@link #loadDrawable(Context)} 237 * @param listener a callback to run on the provided 238 * @param handler {@link Handler} on which to run <code>andThen</code>. 239 */ 240 public void loadDrawableAsync(Context context, final OnDrawableLoadedListener listener, 241 Handler handler) { 242 new LoadDrawableTask(context, handler, listener).runAsync(); 243 } 244 245 /** 246 * Returns a Drawable that can be used to draw the image inside this Icon, constructing it 247 * if necessary. Depending on the type of image, this may not be something you want to do on 248 * the UI thread, so consider using 249 * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead. 250 * 251 * @param context {@link android.content.Context Context} in which to load the drawable; used 252 * to access {@link android.content.res.Resources Resources}, for example. 253 * @return A fresh instance of a drawable for this image, yours to keep. 254 */ 255 public Drawable loadDrawable(Context context) { 256 switch (mType) { 257 case TYPE_BITMAP: 258 return new BitmapDrawable(context.getResources(), getBitmap()); 259 case TYPE_RESOURCE: 260 if (getResources() == null) { 261 if (getResPackage() == null || "android".equals(getResPackage())) { 262 mObj1 = Resources.getSystem(); 263 } else { 264 final PackageManager pm = context.getPackageManager(); 265 try { 266 mObj1 = pm.getResourcesForApplication(getResPackage()); 267 } catch (PackageManager.NameNotFoundException e) { 268 Log.e(TAG, String.format("Unable to find pkg=%s", 269 getResPackage()), 270 e); 271 break; 272 } 273 } 274 } 275 try { 276 return getResources().getDrawable(getResId(), context.getTheme()); 277 } catch (RuntimeException e) { 278 Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s", 279 getResId(), 280 getResPackage()), 281 e); 282 } 283 case TYPE_DATA: 284 return new BitmapDrawable(context.getResources(), 285 BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) 286 ); 287 case TYPE_URI: 288 final Uri uri = getUri(); 289 final String scheme = uri.getScheme(); 290 InputStream is = null; 291 if (ContentResolver.SCHEME_CONTENT.equals(scheme) 292 || ContentResolver.SCHEME_FILE.equals(scheme)) { 293 try { 294 is = context.getContentResolver().openInputStream(uri); 295 } catch (Exception e) { 296 Log.w(TAG, "Unable to load image from URI: " + uri, e); 297 } 298 } else { 299 try { 300 is = new FileInputStream(new File(mString1)); 301 } catch (FileNotFoundException e) { 302 Log.w(TAG, "Unable to load image from path: " + uri, e); 303 } 304 } 305 if (is != null) { 306 return new BitmapDrawable(context.getResources(), 307 BitmapFactory.decodeStream(is)); 308 } 309 break; 310 } 311 return null; 312 } 313 314 /** 315 * Load the requested resources under the given userId, if the system allows it, 316 * before actually loading the drawable. 317 * 318 * @hide 319 */ 320 public Drawable loadDrawableAsUser(Context context, int userId) { 321 if (mType == TYPE_RESOURCE) { 322 if (getResources() == null 323 && getResPackage() != null 324 && !(getResPackage().equals("android"))) { 325 final PackageManager pm = context.getPackageManager(); 326 try { 327 mObj1 = pm.getResourcesForApplicationAsUser(getResPackage(), userId); 328 } catch (PackageManager.NameNotFoundException e) { 329 Log.e(TAG, String.format("Unable to find pkg=%s user=%d", 330 getResPackage(), 331 userId), 332 e); 333 } 334 } 335 } 336 return loadDrawable(context); 337 } 338 339 /** 340 * Writes a serialized version of an Icon to the specified stream. 341 * 342 * @param stream The stream on which to serialize the Icon. 343 * @hide 344 */ 345 public void writeToStream(OutputStream stream) throws IOException { 346 DataOutputStream dataStream = new DataOutputStream(stream); 347 348 dataStream.writeInt(VERSION_STREAM_SERIALIZER); 349 dataStream.writeByte(mType); 350 351 switch (mType) { 352 case TYPE_BITMAP: 353 getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream); 354 break; 355 case TYPE_DATA: 356 dataStream.writeInt(getDataLength()); 357 dataStream.write(getDataBytes(), getDataOffset(), getDataLength()); 358 break; 359 case TYPE_RESOURCE: 360 dataStream.writeUTF(getResPackage()); 361 dataStream.writeInt(getResId()); 362 break; 363 case TYPE_URI: 364 dataStream.writeUTF(getUriString()); 365 break; 366 } 367 } 368 369 private Icon(int mType) { 370 this.mType = mType; 371 } 372 373 /** 374 * Create an Icon from the specified stream. 375 * 376 * @param stream The input stream from which to reconstruct the Icon. 377 * @hide 378 */ 379 public static Icon createFromStream(InputStream stream) throws IOException { 380 DataInputStream inputStream = new DataInputStream(stream); 381 382 final int version = inputStream.readInt(); 383 if (version >= VERSION_STREAM_SERIALIZER) { 384 final int type = inputStream.readByte(); 385 switch (type) { 386 case TYPE_BITMAP: 387 return createWithBitmap(BitmapFactory.decodeStream(inputStream)); 388 case TYPE_DATA: 389 final int length = inputStream.readInt(); 390 final byte[] data = new byte[length]; 391 inputStream.read(data, 0 /* offset */, length); 392 return createWithData(data, 0 /* offset */, length); 393 case TYPE_RESOURCE: 394 final String packageName = inputStream.readUTF(); 395 final int resId = inputStream.readInt(); 396 return createWithResource(packageName, resId); 397 case TYPE_URI: 398 final String uriOrPath = inputStream.readUTF(); 399 return createWithContentUri(uriOrPath); 400 } 401 } 402 return null; 403 } 404 405 /** 406 * Create an Icon pointing to a drawable resource. 407 * @param res Resources for a package containing the resource in question 408 * @param resId ID of the drawable resource 409 */ 410 public static Icon createWithResource(Resources res, @DrawableRes int resId) { 411 if (res == null) { 412 throw new IllegalArgumentException("Resource must not be null."); 413 } 414 final Icon rep = new Icon(TYPE_RESOURCE); 415 rep.mObj1 = res; 416 rep.mInt1 = resId; 417 rep.mString1 = res.getResourcePackageName(resId); 418 return rep; 419 } 420 421 /** 422 * Create an Icon pointing to a drawable resource. 423 * @param resPackage Name of the package containing the resource in question 424 * @param resId ID of the drawable resource 425 */ 426 public static Icon createWithResource(String resPackage, @DrawableRes int resId) { 427 if (resPackage == null) { 428 throw new IllegalArgumentException("Resource package name must not be null."); 429 } 430 final Icon rep = new Icon(TYPE_RESOURCE); 431 rep.mInt1 = resId; 432 rep.mString1 = resPackage; 433 return rep; 434 } 435 436 /** 437 * Create an Icon pointing to a bitmap in memory. 438 * @param bits A valid {@link android.graphics.Bitmap} object 439 */ 440 public static Icon createWithBitmap(Bitmap bits) { 441 if (bits == null) { 442 throw new IllegalArgumentException("Bitmap must not be null."); 443 } 444 final Icon rep = new Icon(TYPE_BITMAP); 445 rep.mObj1 = bits; 446 return rep; 447 } 448 449 /** 450 * Create an Icon pointing to a compressed bitmap stored in a byte array. 451 * @param data Byte array storing compressed bitmap data of a type that 452 * {@link android.graphics.BitmapFactory} 453 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 454 * @param offset Offset into <code>data</code> at which the bitmap data starts 455 * @param length Length of the bitmap data 456 */ 457 public static Icon createWithData(byte[] data, int offset, int length) { 458 if (data == null) { 459 throw new IllegalArgumentException("Data must not be null."); 460 } 461 final Icon rep = new Icon(TYPE_DATA); 462 rep.mObj1 = data; 463 rep.mInt1 = length; 464 rep.mInt2 = offset; 465 return rep; 466 } 467 468 /** 469 * Create an Icon pointing to an image file specified by URI. 470 * 471 * @param uri A uri referring to local content:// or file:// image data. 472 */ 473 public static Icon createWithContentUri(String uri) { 474 if (uri == null) { 475 throw new IllegalArgumentException("Uri must not be null."); 476 } 477 final Icon rep = new Icon(TYPE_URI); 478 rep.mString1 = uri; 479 return rep; 480 } 481 482 /** 483 * Create an Icon pointing to an image file specified by URI. 484 * 485 * @param uri A uri referring to local content:// or file:// image data. 486 */ 487 public static Icon createWithContentUri(Uri uri) { 488 if (uri == null) { 489 throw new IllegalArgumentException("Uri must not be null."); 490 } 491 final Icon rep = new Icon(TYPE_URI); 492 rep.mString1 = uri.toString(); 493 return rep; 494 } 495 496 /** 497 * Create an Icon pointing to an image file specified by path. 498 * 499 * @param path A path to a file that contains compressed bitmap data of 500 * a type that {@link android.graphics.BitmapFactory} can decode. 501 */ 502 public static Icon createWithFilePath(String path) { 503 if (path == null) { 504 throw new IllegalArgumentException("Path must not be null."); 505 } 506 final Icon rep = new Icon(TYPE_URI); 507 rep.mString1 = path; 508 return rep; 509 } 510 511 @Override 512 public String toString() { 513 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 514 switch (mType) { 515 case TYPE_BITMAP: 516 sb.append(" size=") 517 .append(getBitmap().getWidth()) 518 .append("x") 519 .append(getBitmap().getHeight()); 520 break; 521 case TYPE_RESOURCE: 522 sb.append(" pkg=") 523 .append(getResPackage()) 524 .append(" id=") 525 .append(String.format("%08x", getResId())); 526 break; 527 case TYPE_DATA: 528 sb.append(" len=").append(getDataLength()); 529 if (getDataOffset() != 0) { 530 sb.append(" off=").append(getDataOffset()); 531 } 532 break; 533 case TYPE_URI: 534 sb.append(" uri=").append(getUriString()); 535 break; 536 } 537 sb.append(")"); 538 return sb.toString(); 539 } 540 541 /** 542 * Parcelable interface 543 */ 544 public int describeContents() { 545 return (mType == TYPE_BITMAP || mType == TYPE_DATA) 546 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 547 } 548 549 // ===== Parcelable interface ====== 550 551 private Icon(Parcel in) { 552 this(in.readInt()); 553 switch (mType) { 554 case TYPE_BITMAP: 555 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 556 mObj1 = bits; 557 break; 558 case TYPE_RESOURCE: 559 final String pkg = in.readString(); 560 final int resId = in.readInt(); 561 mString1 = pkg; 562 mInt1 = resId; 563 break; 564 case TYPE_DATA: 565 final int len = in.readInt(); 566 final byte[] a = in.readBlob(); 567 if (len != a.length) { 568 throw new RuntimeException("internal unparceling error: blob length (" 569 + a.length + ") != expected length (" + len + ")"); 570 } 571 mInt1 = len; 572 mObj1 = a; 573 break; 574 case TYPE_URI: 575 final String uri = in.readString(); 576 mString1 = uri; 577 break; 578 default: 579 throw new RuntimeException("invalid " 580 + this.getClass().getSimpleName() + " type in parcel: " + mType); 581 } 582 } 583 584 @Override 585 public void writeToParcel(Parcel dest, int flags) { 586 switch (mType) { 587 case TYPE_BITMAP: 588 final Bitmap bits = getBitmap(); 589 dest.writeInt(TYPE_BITMAP); 590 getBitmap().writeToParcel(dest, flags); 591 break; 592 case TYPE_RESOURCE: 593 dest.writeInt(TYPE_RESOURCE); 594 dest.writeString(getResPackage()); 595 dest.writeInt(getResId()); 596 break; 597 case TYPE_DATA: 598 dest.writeInt(TYPE_DATA); 599 dest.writeInt(getDataLength()); 600 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 601 break; 602 case TYPE_URI: 603 dest.writeInt(TYPE_URI); 604 dest.writeString(getUriString()); 605 break; 606 } 607 } 608 609 public static final Parcelable.Creator<Icon> CREATOR 610 = new Parcelable.Creator<Icon>() { 611 public Icon createFromParcel(Parcel in) { 612 return new Icon(in); 613 } 614 615 public Icon[] newArray(int size) { 616 return new Icon[size]; 617 } 618 }; 619 620 /** 621 * Implement this interface to receive a callback when 622 * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync} 623 * is finished and your Drawable is ready. 624 */ 625 public interface OnDrawableLoadedListener { 626 void onDrawableLoaded(Drawable d); 627 } 628 629 /** 630 * Wrapper around loadDrawable that does its work on a pooled thread and then 631 * fires back the given (targeted) Message. 632 */ 633 private class LoadDrawableTask implements Runnable { 634 final Context mContext; 635 final Message mMessage; 636 637 public LoadDrawableTask(Context context, final Handler handler, 638 final OnDrawableLoadedListener listener) { 639 mContext = context; 640 mMessage = Message.obtain(handler, new Runnable() { 641 @Override 642 public void run() { 643 listener.onDrawableLoaded((Drawable) mMessage.obj); 644 } 645 }); 646 } 647 648 public LoadDrawableTask(Context context, Message message) { 649 mContext = context; 650 mMessage = message; 651 } 652 653 @Override 654 public void run() { 655 mMessage.obj = loadDrawable(mContext); 656 mMessage.sendToTarget(); 657 } 658 659 public void runAsync() { 660 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 661 } 662 } 663} 664