Icon.java revision 877d696c382ecb8a97972450c8819536641a963c
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.os.UserHandle; 33import android.util.Log; 34 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 Context Context} in which to load the drawable; see 181 * {@link #loadDrawable(Context)} 182 * @param listener a callback to run on the provided 183 * @param handler {@link Handler} on which to run <code>andThen</code>. 184 */ 185 public void loadDrawableAsync(Context context, final OnDrawableLoadedListener listener, 186 Handler handler) { 187 new LoadDrawableTask(context, handler, listener).runAsync(); 188 } 189 190 /** 191 * Returns a Drawable that can be used to draw the image inside this Icon, constructing it 192 * if necessary. Depending on the type of image, this may not be something you want to do on 193 * the UI thread, so consider using 194 * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead. 195 * 196 * @param context {@link android.content.Context Context} in which to load the drawable; used 197 * to access {@link android.content.res.Resources Resources}, for example. 198 * @return A fresh instance of a drawable for this image, yours to keep. 199 */ 200 public Drawable loadDrawable(Context context) { 201 switch (mType) { 202 case TYPE_BITMAP: 203 return new BitmapDrawable(context.getResources(), getBitmap()); 204 case TYPE_RESOURCE: 205 if (getResources() == null) { 206 if (getResPackage() == null || "android".equals(getResPackage())) { 207 mObj1 = Resources.getSystem(); 208 } else { 209 final PackageManager pm = context.getPackageManager(); 210 try { 211 mObj1 = pm.getResourcesForApplication(getResPackage()); 212 } catch (PackageManager.NameNotFoundException e) { 213 Log.e(TAG, String.format("Unable to find pkg=%s", 214 getResPackage()), 215 e); 216 break; 217 } 218 } 219 } 220 try { 221 return getResources().getDrawable(getResId(), context.getTheme()); 222 } catch (RuntimeException e) { 223 Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s", 224 getResId(), 225 getResPackage()), 226 e); 227 } 228 case TYPE_DATA: 229 return new BitmapDrawable(context.getResources(), 230 BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) 231 ); 232 case TYPE_URI: 233 final Uri uri = getUri(); 234 final String scheme = uri.getScheme(); 235 InputStream is = null; 236 if (ContentResolver.SCHEME_CONTENT.equals(scheme) 237 || ContentResolver.SCHEME_FILE.equals(scheme)) { 238 try { 239 is = context.getContentResolver().openInputStream(uri); 240 } catch (Exception e) { 241 Log.w(TAG, "Unable to load image from URI: " + uri, e); 242 } 243 } else { 244 try { 245 is = new FileInputStream(new File(mString1)); 246 } catch (FileNotFoundException e) { 247 Log.w(TAG, "Unable to load image from path: " + uri, e); 248 } 249 } 250 if (is != null) { 251 return new BitmapDrawable(context.getResources(), 252 BitmapFactory.decodeStream(is)); 253 } 254 break; 255 } 256 return null; 257 } 258 259 /** 260 * Load the requested resources under the given userId, if the system allows it, 261 * before actually loading the drawable. 262 * 263 * @hide 264 */ 265 public Drawable loadDrawableAsUser(Context context, int userId) { 266 if (mType == TYPE_RESOURCE) { 267 if (getResources() == null 268 && getResPackage() != null 269 && !(getResPackage().equals("android"))) { 270 final PackageManager pm = context.getPackageManager(); 271 try { 272 mObj1 = pm.getResourcesForApplicationAsUser(getResPackage(), userId); 273 } catch (PackageManager.NameNotFoundException e) { 274 Log.e(TAG, String.format("Unable to find pkg=%s user=%d", 275 getResPackage(), 276 userId), 277 e); 278 } 279 } 280 } 281 return loadDrawable(context); 282 } 283 284 private Icon(int mType) { 285 this.mType = mType; 286 } 287 288 /** 289 * Create an Icon pointing to a drawable resource. 290 * @param res Resources for a package containing the resource in question 291 * @param resId ID of the drawable resource 292 */ 293 public static Icon createWithResource(Resources res, @DrawableRes int resId) { 294 final Icon rep = new Icon(TYPE_RESOURCE); 295 rep.mObj1 = res; 296 rep.mInt1 = resId; 297 rep.mString1 = res.getResourcePackageName(resId); 298 return rep; 299 } 300 301 /** 302 * Create an Icon pointing to a drawable resource. 303 * @param resPackage Name of the package containing the resource in question 304 * @param resId ID of the drawable resource 305 */ 306 public static Icon createWithResource(String resPackage, @DrawableRes int resId) { 307 final Icon rep = new Icon(TYPE_RESOURCE); 308 rep.mInt1 = resId; 309 rep.mString1 = resPackage; 310 return rep; 311 } 312 313 /** 314 * Create an Icon pointing to a bitmap in memory. 315 * @param bits A valid {@link android.graphics.Bitmap} object 316 */ 317 public static Icon createWithBitmap(Bitmap bits) { 318 final Icon rep = new Icon(TYPE_BITMAP); 319 rep.mObj1 = bits; 320 return rep; 321 } 322 323 /** 324 * Create an Icon pointing to a compressed bitmap stored in a byte array. 325 * @param data Byte array storing compressed bitmap data of a type that 326 * {@link android.graphics.BitmapFactory} 327 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 328 * @param offset Offset into <code>data</code> at which the bitmap data starts 329 * @param length Length of the bitmap data 330 */ 331 public static Icon createWithData(byte[] data, int offset, int length) { 332 final Icon rep = new Icon(TYPE_DATA); 333 rep.mObj1 = data; 334 rep.mInt1 = length; 335 rep.mInt2 = offset; 336 return rep; 337 } 338 339 /** 340 * Create an Icon pointing to an image file specified by URI. 341 * 342 * @param uri A uri referring to local content:// or file:// image data. 343 */ 344 public static Icon createWithContentUri(String uri) { 345 final Icon rep = new Icon(TYPE_URI); 346 rep.mString1 = uri; 347 return rep; 348 } 349 350 /** 351 * Create an Icon pointing to an image file specified by URI. 352 * 353 * @param uri A uri referring to local content:// or file:// image data. 354 */ 355 public static Icon createWithContentUri(Uri uri) { 356 final Icon rep = new Icon(TYPE_URI); 357 rep.mString1 = uri.toString(); 358 return rep; 359 } 360 361 /** 362 * Create an Icon pointing to an image file specified by path. 363 * 364 * @param path A path to a file that contains compressed bitmap data of 365 * a type that {@link android.graphics.BitmapFactory} can decode. 366 */ 367 public static Icon createWithFilePath(String path) { 368 final Icon rep = new Icon(TYPE_URI); 369 rep.mString1 = path; 370 return rep; 371 } 372 373 @Override 374 public String toString() { 375 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 376 switch (mType) { 377 case TYPE_BITMAP: 378 sb.append(" size=") 379 .append(getBitmap().getWidth()) 380 .append("x") 381 .append(getBitmap().getHeight()); 382 break; 383 case TYPE_RESOURCE: 384 sb.append(" pkg=") 385 .append(getResPackage()) 386 .append(" id=") 387 .append(String.format("%08x", getResId())); 388 break; 389 case TYPE_DATA: 390 sb.append(" len=").append(getDataLength()); 391 if (getDataOffset() != 0) { 392 sb.append(" off=").append(getDataOffset()); 393 } 394 break; 395 case TYPE_URI: 396 sb.append(" uri=").append(getUriString()); 397 break; 398 } 399 sb.append(")"); 400 return sb.toString(); 401 } 402 403 /** 404 * Parcelable interface 405 */ 406 public int describeContents() { 407 return (mType == TYPE_BITMAP || mType == TYPE_DATA) 408 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 409 } 410 411 // ===== Parcelable interface ====== 412 413 private Icon(Parcel in) { 414 this(in.readInt()); 415 switch (mType) { 416 case TYPE_BITMAP: 417 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 418 mObj1 = bits; 419 break; 420 case TYPE_RESOURCE: 421 final String pkg = in.readString(); 422 final int resId = in.readInt(); 423 mString1 = pkg; 424 mInt1 = resId; 425 break; 426 case TYPE_DATA: 427 final int len = in.readInt(); 428 final byte[] a = in.readBlob(); 429 if (len != a.length) { 430 throw new RuntimeException("internal unparceling error: blob length (" 431 + a.length + ") != expected length (" + len + ")"); 432 } 433 mInt1 = len; 434 mObj1 = a; 435 break; 436 case TYPE_URI: 437 final String uri = in.readString(); 438 mString1 = uri; 439 break; 440 default: 441 throw new RuntimeException("invalid " 442 + this.getClass().getSimpleName() + " type in parcel: " + mType); 443 } 444 } 445 446 @Override 447 public void writeToParcel(Parcel dest, int flags) { 448 switch (mType) { 449 case TYPE_BITMAP: 450 final Bitmap bits = getBitmap(); 451 dest.writeInt(TYPE_BITMAP); 452 getBitmap().writeToParcel(dest, flags); 453 break; 454 case TYPE_RESOURCE: 455 dest.writeInt(TYPE_RESOURCE); 456 dest.writeString(getResPackage()); 457 dest.writeInt(getResId()); 458 break; 459 case TYPE_DATA: 460 dest.writeInt(TYPE_DATA); 461 dest.writeInt(getDataLength()); 462 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 463 break; 464 case TYPE_URI: 465 dest.writeInt(TYPE_URI); 466 dest.writeString(getUriString()); 467 break; 468 } 469 } 470 471 public static final Parcelable.Creator<Icon> CREATOR 472 = new Parcelable.Creator<Icon>() { 473 public Icon createFromParcel(Parcel in) { 474 return new Icon(in); 475 } 476 477 public Icon[] newArray(int size) { 478 return new Icon[size]; 479 } 480 }; 481 482 /** 483 * Implement this interface to receive a callback when 484 * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync} 485 * is finished and your Drawable is ready. 486 */ 487 public interface OnDrawableLoadedListener { 488 void onDrawableLoaded(Drawable d); 489 } 490 491 /** 492 * Wrapper around loadDrawable that does its work on a pooled thread and then 493 * fires back the given (targeted) Message. 494 */ 495 private class LoadDrawableTask implements Runnable { 496 final Context mContext; 497 final Message mMessage; 498 499 public LoadDrawableTask(Context context, final Handler handler, 500 final OnDrawableLoadedListener listener) { 501 mContext = context; 502 mMessage = Message.obtain(handler, new Runnable() { 503 @Override 504 public void run() { 505 listener.onDrawableLoaded((Drawable) mMessage.obj); 506 } 507 }); 508 } 509 510 public LoadDrawableTask(Context context, Message message) { 511 mContext = context; 512 mMessage = message; 513 } 514 515 @Override 516 public void run() { 517 mMessage.obj = loadDrawable(mContext); 518 mMessage.sendToTarget(); 519 } 520 521 public void runAsync() { 522 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 523 } 524 } 525} 526