Icon.java revision d63f9321e62064660d426efd5abbd885c4a24652
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 break; 284 case TYPE_DATA: 285 return new BitmapDrawable(context.getResources(), 286 BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) 287 ); 288 case TYPE_URI: 289 final Uri uri = getUri(); 290 final String scheme = uri.getScheme(); 291 InputStream is = null; 292 if (ContentResolver.SCHEME_CONTENT.equals(scheme) 293 || ContentResolver.SCHEME_FILE.equals(scheme)) { 294 try { 295 is = context.getContentResolver().openInputStream(uri); 296 } catch (Exception e) { 297 Log.w(TAG, "Unable to load image from URI: " + uri, e); 298 } 299 } else { 300 try { 301 is = new FileInputStream(new File(mString1)); 302 } catch (FileNotFoundException e) { 303 Log.w(TAG, "Unable to load image from path: " + uri, e); 304 } 305 } 306 if (is != null) { 307 return new BitmapDrawable(context.getResources(), 308 BitmapFactory.decodeStream(is)); 309 } 310 break; 311 } 312 return null; 313 } 314 315 /** 316 * Load the requested resources under the given userId, if the system allows it, 317 * before actually loading the drawable. 318 * 319 * @hide 320 */ 321 public Drawable loadDrawableAsUser(Context context, int userId) { 322 if (mType == TYPE_RESOURCE) { 323 if (getResources() == null 324 && getResPackage() != null 325 && !(getResPackage().equals("android"))) { 326 final PackageManager pm = context.getPackageManager(); 327 try { 328 mObj1 = pm.getResourcesForApplicationAsUser(getResPackage(), userId); 329 } catch (PackageManager.NameNotFoundException e) { 330 Log.e(TAG, String.format("Unable to find pkg=%s user=%d", 331 getResPackage(), 332 userId), 333 e); 334 } 335 } 336 } 337 return loadDrawable(context); 338 } 339 340 /** 341 * Writes a serialized version of an Icon to the specified stream. 342 * 343 * @param stream The stream on which to serialize the Icon. 344 * @hide 345 */ 346 public void writeToStream(OutputStream stream) throws IOException { 347 DataOutputStream dataStream = new DataOutputStream(stream); 348 349 dataStream.writeInt(VERSION_STREAM_SERIALIZER); 350 dataStream.writeByte(mType); 351 352 switch (mType) { 353 case TYPE_BITMAP: 354 getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream); 355 break; 356 case TYPE_DATA: 357 dataStream.writeInt(getDataLength()); 358 dataStream.write(getDataBytes(), getDataOffset(), getDataLength()); 359 break; 360 case TYPE_RESOURCE: 361 dataStream.writeUTF(getResPackage()); 362 dataStream.writeInt(getResId()); 363 break; 364 case TYPE_URI: 365 dataStream.writeUTF(getUriString()); 366 break; 367 } 368 } 369 370 private Icon(int mType) { 371 this.mType = mType; 372 } 373 374 /** 375 * Create an Icon from the specified stream. 376 * 377 * @param stream The input stream from which to reconstruct the Icon. 378 * @hide 379 */ 380 public static Icon createFromStream(InputStream stream) throws IOException { 381 DataInputStream inputStream = new DataInputStream(stream); 382 383 final int version = inputStream.readInt(); 384 if (version >= VERSION_STREAM_SERIALIZER) { 385 final int type = inputStream.readByte(); 386 switch (type) { 387 case TYPE_BITMAP: 388 return createWithBitmap(BitmapFactory.decodeStream(inputStream)); 389 case TYPE_DATA: 390 final int length = inputStream.readInt(); 391 final byte[] data = new byte[length]; 392 inputStream.read(data, 0 /* offset */, length); 393 return createWithData(data, 0 /* offset */, length); 394 case TYPE_RESOURCE: 395 final String packageName = inputStream.readUTF(); 396 final int resId = inputStream.readInt(); 397 return createWithResource(packageName, resId); 398 case TYPE_URI: 399 final String uriOrPath = inputStream.readUTF(); 400 return createWithContentUri(uriOrPath); 401 } 402 } 403 return null; 404 } 405 406 /** 407 * Create an Icon pointing to a drawable resource. 408 * @param context The context for the application whose resources should be used to resolve the 409 * given resource ID. 410 * @param resId ID of the drawable resource 411 */ 412 public static Icon createWithResource(Context context, @DrawableRes int resId) { 413 final Icon rep = new Icon(TYPE_RESOURCE); 414 rep.mInt1 = resId; 415 rep.mString1 = context.getPackageName(); 416 return rep; 417 } 418 419 /** 420 * Version of createWithResource that takes Resources. Do not use. 421 * @hide 422 */ 423 public static Icon createWithResource(Resources res, @DrawableRes int resId) { 424 if (res == null) { 425 throw new IllegalArgumentException("Resource must not be null."); 426 } 427 final Icon rep = new Icon(TYPE_RESOURCE); 428 rep.mInt1 = resId; 429 rep.mString1 = res.getResourcePackageName(resId); 430 return rep; 431 } 432 433 /** 434 * Create an Icon pointing to a drawable resource. 435 * @param resPackage Name of the package containing the resource in question 436 * @param resId ID of the drawable resource 437 */ 438 public static Icon createWithResource(String resPackage, @DrawableRes int resId) { 439 if (resPackage == null) { 440 throw new IllegalArgumentException("Resource package name must not be null."); 441 } 442 final Icon rep = new Icon(TYPE_RESOURCE); 443 rep.mInt1 = resId; 444 rep.mString1 = resPackage; 445 return rep; 446 } 447 448 /** 449 * Create an Icon pointing to a bitmap in memory. 450 * @param bits A valid {@link android.graphics.Bitmap} object 451 */ 452 public static Icon createWithBitmap(Bitmap bits) { 453 if (bits == null) { 454 throw new IllegalArgumentException("Bitmap must not be null."); 455 } 456 final Icon rep = new Icon(TYPE_BITMAP); 457 rep.mObj1 = bits; 458 return rep; 459 } 460 461 /** 462 * Create an Icon pointing to a compressed bitmap stored in a byte array. 463 * @param data Byte array storing compressed bitmap data of a type that 464 * {@link android.graphics.BitmapFactory} 465 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 466 * @param offset Offset into <code>data</code> at which the bitmap data starts 467 * @param length Length of the bitmap data 468 */ 469 public static Icon createWithData(byte[] data, int offset, int length) { 470 if (data == null) { 471 throw new IllegalArgumentException("Data must not be null."); 472 } 473 final Icon rep = new Icon(TYPE_DATA); 474 rep.mObj1 = data; 475 rep.mInt1 = length; 476 rep.mInt2 = offset; 477 return rep; 478 } 479 480 /** 481 * Create an Icon pointing to an image file specified by URI. 482 * 483 * @param uri A uri referring to local content:// or file:// image data. 484 */ 485 public static Icon createWithContentUri(String uri) { 486 if (uri == null) { 487 throw new IllegalArgumentException("Uri must not be null."); 488 } 489 final Icon rep = new Icon(TYPE_URI); 490 rep.mString1 = uri; 491 return rep; 492 } 493 494 /** 495 * Create an Icon pointing to an image file specified by URI. 496 * 497 * @param uri A uri referring to local content:// or file:// image data. 498 */ 499 public static Icon createWithContentUri(Uri uri) { 500 if (uri == null) { 501 throw new IllegalArgumentException("Uri must not be null."); 502 } 503 final Icon rep = new Icon(TYPE_URI); 504 rep.mString1 = uri.toString(); 505 return rep; 506 } 507 508 /** 509 * Create an Icon pointing to an image file specified by path. 510 * 511 * @param path A path to a file that contains compressed bitmap data of 512 * a type that {@link android.graphics.BitmapFactory} can decode. 513 */ 514 public static Icon createWithFilePath(String path) { 515 if (path == null) { 516 throw new IllegalArgumentException("Path must not be null."); 517 } 518 final Icon rep = new Icon(TYPE_URI); 519 rep.mString1 = path; 520 return rep; 521 } 522 523 @Override 524 public String toString() { 525 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 526 switch (mType) { 527 case TYPE_BITMAP: 528 sb.append(" size=") 529 .append(getBitmap().getWidth()) 530 .append("x") 531 .append(getBitmap().getHeight()); 532 break; 533 case TYPE_RESOURCE: 534 sb.append(" pkg=") 535 .append(getResPackage()) 536 .append(" id=") 537 .append(String.format("0x%08x", getResId())); 538 break; 539 case TYPE_DATA: 540 sb.append(" len=").append(getDataLength()); 541 if (getDataOffset() != 0) { 542 sb.append(" off=").append(getDataOffset()); 543 } 544 break; 545 case TYPE_URI: 546 sb.append(" uri=").append(getUriString()); 547 break; 548 } 549 sb.append(")"); 550 return sb.toString(); 551 } 552 553 /** 554 * Parcelable interface 555 */ 556 public int describeContents() { 557 return (mType == TYPE_BITMAP || mType == TYPE_DATA) 558 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 559 } 560 561 // ===== Parcelable interface ====== 562 563 private Icon(Parcel in) { 564 this(in.readInt()); 565 switch (mType) { 566 case TYPE_BITMAP: 567 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 568 mObj1 = bits; 569 break; 570 case TYPE_RESOURCE: 571 final String pkg = in.readString(); 572 final int resId = in.readInt(); 573 mString1 = pkg; 574 mInt1 = resId; 575 break; 576 case TYPE_DATA: 577 final int len = in.readInt(); 578 final byte[] a = in.readBlob(); 579 if (len != a.length) { 580 throw new RuntimeException("internal unparceling error: blob length (" 581 + a.length + ") != expected length (" + len + ")"); 582 } 583 mInt1 = len; 584 mObj1 = a; 585 break; 586 case TYPE_URI: 587 final String uri = in.readString(); 588 mString1 = uri; 589 break; 590 default: 591 throw new RuntimeException("invalid " 592 + this.getClass().getSimpleName() + " type in parcel: " + mType); 593 } 594 } 595 596 @Override 597 public void writeToParcel(Parcel dest, int flags) { 598 switch (mType) { 599 case TYPE_BITMAP: 600 final Bitmap bits = getBitmap(); 601 dest.writeInt(TYPE_BITMAP); 602 getBitmap().writeToParcel(dest, flags); 603 break; 604 case TYPE_RESOURCE: 605 dest.writeInt(TYPE_RESOURCE); 606 dest.writeString(getResPackage()); 607 dest.writeInt(getResId()); 608 break; 609 case TYPE_DATA: 610 dest.writeInt(TYPE_DATA); 611 dest.writeInt(getDataLength()); 612 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 613 break; 614 case TYPE_URI: 615 dest.writeInt(TYPE_URI); 616 dest.writeString(getUriString()); 617 break; 618 } 619 } 620 621 public static final Parcelable.Creator<Icon> CREATOR 622 = new Parcelable.Creator<Icon>() { 623 public Icon createFromParcel(Parcel in) { 624 return new Icon(in); 625 } 626 627 public Icon[] newArray(int size) { 628 return new Icon[size]; 629 } 630 }; 631 632 /** 633 * Implement this interface to receive a callback when 634 * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync} 635 * is finished and your Drawable is ready. 636 */ 637 public interface OnDrawableLoadedListener { 638 void onDrawableLoaded(Drawable d); 639 } 640 641 /** 642 * Wrapper around loadDrawable that does its work on a pooled thread and then 643 * fires back the given (targeted) Message. 644 */ 645 private class LoadDrawableTask implements Runnable { 646 final Context mContext; 647 final Message mMessage; 648 649 public LoadDrawableTask(Context context, final Handler handler, 650 final OnDrawableLoadedListener listener) { 651 mContext = context; 652 mMessage = Message.obtain(handler, new Runnable() { 653 @Override 654 public void run() { 655 listener.onDrawableLoaded((Drawable) mMessage.obj); 656 } 657 }); 658 } 659 660 public LoadDrawableTask(Context context, Message message) { 661 mContext = context; 662 mMessage = message; 663 } 664 665 @Override 666 public void run() { 667 mMessage.obj = loadDrawable(mContext); 668 mMessage.sendToTarget(); 669 } 670 671 public void runAsync() { 672 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 673 } 674 } 675} 676