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