1 /*
2  * Copyright (C) 2011 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 
17 package android.view;
18 
19 import com.android.internal.util.XmlUtils;
20 
21 import android.annotation.XmlRes;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.content.res.XmlResourceParser;
26 import android.graphics.Bitmap;
27 import android.graphics.drawable.BitmapDrawable;
28 import android.graphics.drawable.Drawable;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import android.util.Log;
32 
33 /**
34  * Represents an icon that can be used as a mouse pointer.
35  * <p>
36  * Pointer icons can be provided either by the system using system styles,
37  * or by applications using bitmaps or application resources.
38  * </p>
39  *
40  * @hide
41  */
42 public final class PointerIcon implements Parcelable {
43     private static final String TAG = "PointerIcon";
44 
45     /** Style constant: Custom icon with a user-supplied bitmap. */
46     public static final int STYLE_CUSTOM = -1;
47 
48     /** Style constant: Null icon.  It has no bitmap. */
49     public static final int STYLE_NULL = 0;
50 
51     /** Style constant: Arrow icon.  (Default mouse pointer) */
52     public static final int STYLE_ARROW = 1000;
53 
54     /** {@hide} Style constant: Spot hover icon for touchpads. */
55     public static final int STYLE_SPOT_HOVER = 2000;
56 
57     /** {@hide} Style constant: Spot touch icon for touchpads. */
58     public static final int STYLE_SPOT_TOUCH = 2001;
59 
60     /** {@hide} Style constant: Spot anchor icon for touchpads. */
61     public static final int STYLE_SPOT_ANCHOR = 2002;
62 
63     // OEM private styles should be defined starting at this range to avoid
64     // conflicts with any system styles that may be defined in the future.
65     private static final int STYLE_OEM_FIRST = 10000;
66 
67     // The default pointer icon.
68     private static final int STYLE_DEFAULT = STYLE_ARROW;
69 
70     private static final PointerIcon gNullIcon = new PointerIcon(STYLE_NULL);
71 
72     private final int mStyle;
73     private int mSystemIconResourceId;
74     private Bitmap mBitmap;
75     private float mHotSpotX;
76     private float mHotSpotY;
77 
PointerIcon(int style)78     private PointerIcon(int style) {
79         mStyle = style;
80     }
81 
82     /**
83      * Gets a special pointer icon that has no bitmap.
84      *
85      * @return The null pointer icon.
86      *
87      * @see #STYLE_NULL
88      */
getNullIcon()89     public static PointerIcon getNullIcon() {
90         return gNullIcon;
91     }
92 
93     /**
94      * Gets the default pointer icon.
95      *
96      * @param context The context.
97      * @return The default pointer icon.
98      *
99      * @throws IllegalArgumentException if context is null.
100      */
getDefaultIcon(Context context)101     public static PointerIcon getDefaultIcon(Context context) {
102         return getSystemIcon(context, STYLE_DEFAULT);
103     }
104 
105     /**
106      * Gets a system pointer icon for the given style.
107      * If style is not recognized, returns the default pointer icon.
108      *
109      * @param context The context.
110      * @param style The pointer icon style.
111      * @return The pointer icon.
112      *
113      * @throws IllegalArgumentException if context is null.
114      */
getSystemIcon(Context context, int style)115     public static PointerIcon getSystemIcon(Context context, int style) {
116         if (context == null) {
117             throw new IllegalArgumentException("context must not be null");
118         }
119 
120         if (style == STYLE_NULL) {
121             return gNullIcon;
122         }
123 
124         int styleIndex = getSystemIconStyleIndex(style);
125         if (styleIndex == 0) {
126             styleIndex = getSystemIconStyleIndex(STYLE_DEFAULT);
127         }
128 
129         TypedArray a = context.obtainStyledAttributes(null,
130                 com.android.internal.R.styleable.Pointer,
131                 com.android.internal.R.attr.pointerStyle, 0);
132         int resourceId = a.getResourceId(styleIndex, -1);
133         a.recycle();
134 
135         if (resourceId == -1) {
136             Log.w(TAG, "Missing theme resources for pointer icon style " + style);
137             return style == STYLE_DEFAULT ? gNullIcon : getSystemIcon(context, STYLE_DEFAULT);
138         }
139 
140         PointerIcon icon = new PointerIcon(style);
141         if ((resourceId & 0xff000000) == 0x01000000) {
142             icon.mSystemIconResourceId = resourceId;
143         } else {
144             icon.loadResource(context, context.getResources(), resourceId);
145         }
146         return icon;
147     }
148 
149     /**
150      * Creates a custom pointer from the given bitmap and hotspot information.
151      *
152      * @param bitmap The bitmap for the icon.
153      * @param hotSpotX The X offset of the pointer icon hotspot in the bitmap.
154      *        Must be within the [0, bitmap.getWidth()) range.
155      * @param hotSpotY The Y offset of the pointer icon hotspot in the bitmap.
156      *        Must be within the [0, bitmap.getHeight()) range.
157      * @return A pointer icon for this bitmap.
158      *
159      * @throws IllegalArgumentException if bitmap is null, or if the x/y hotspot
160      *         parameters are invalid.
161      */
createCustomIcon(Bitmap bitmap, float hotSpotX, float hotSpotY)162     public static PointerIcon createCustomIcon(Bitmap bitmap, float hotSpotX, float hotSpotY) {
163         if (bitmap == null) {
164             throw new IllegalArgumentException("bitmap must not be null");
165         }
166         validateHotSpot(bitmap, hotSpotX, hotSpotY);
167 
168         PointerIcon icon = new PointerIcon(STYLE_CUSTOM);
169         icon.mBitmap = bitmap;
170         icon.mHotSpotX = hotSpotX;
171         icon.mHotSpotY = hotSpotY;
172         return icon;
173     }
174 
175     /**
176      * Loads a custom pointer icon from an XML resource.
177      * <p>
178      * The XML resource should have the following form:
179      * <code>
180      * &lt;?xml version="1.0" encoding="utf-8"?&gt;
181      * &lt;pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
182      *   android:bitmap="@drawable/my_pointer_bitmap"
183      *   android:hotSpotX="24"
184      *   android:hotSpotY="24" /&gt;
185      * </code>
186      * </p>
187      *
188      * @param resources The resources object.
189      * @param resourceId The resource id.
190      * @return The pointer icon.
191      *
192      * @throws IllegalArgumentException if resources is null.
193      * @throws Resources.NotFoundException if the resource was not found or the drawable
194      * linked in the resource was not found.
195      */
loadCustomIcon(Resources resources, @XmlRes int resourceId)196     public static PointerIcon loadCustomIcon(Resources resources, @XmlRes int resourceId) {
197         if (resources == null) {
198             throw new IllegalArgumentException("resources must not be null");
199         }
200 
201         PointerIcon icon = new PointerIcon(STYLE_CUSTOM);
202         icon.loadResource(null, resources, resourceId);
203         return icon;
204     }
205 
206     /**
207      * Loads the bitmap and hotspot information for a pointer icon, if it is not already loaded.
208      * Returns a pointer icon (not necessarily the same instance) with the information filled in.
209      *
210      * @param context The context.
211      * @return The loaded pointer icon.
212      *
213      * @throws IllegalArgumentException if context is null.
214      * @see #isLoaded()
215      * @hide
216      */
load(Context context)217     public PointerIcon load(Context context) {
218         if (context == null) {
219             throw new IllegalArgumentException("context must not be null");
220         }
221 
222         if (mSystemIconResourceId == 0 || mBitmap != null) {
223             return this;
224         }
225 
226         PointerIcon result = new PointerIcon(mStyle);
227         result.mSystemIconResourceId = mSystemIconResourceId;
228         result.loadResource(context, context.getResources(), mSystemIconResourceId);
229         return result;
230     }
231 
232     /**
233      * Returns true if the pointer icon style is {@link #STYLE_NULL}.
234      *
235      * @return True if the pointer icon style is {@link #STYLE_NULL}.
236      */
isNullIcon()237     public boolean isNullIcon() {
238         return mStyle == STYLE_NULL;
239     }
240 
241     /**
242      * Returns true if the pointer icon has been loaded and its bitmap and hotspot
243      * information are available.
244      *
245      * @return True if the pointer icon is loaded.
246      * @see #load(Context)
247      */
isLoaded()248     public boolean isLoaded() {
249         return mBitmap != null || mStyle == STYLE_NULL;
250     }
251 
252     /**
253      * Gets the style of the pointer icon.
254      *
255      * @return The pointer icon style.
256      */
getStyle()257     public int getStyle() {
258         return mStyle;
259     }
260 
261     /**
262      * Gets the bitmap of the pointer icon.
263      *
264      * @return The pointer icon bitmap, or null if the style is {@link #STYLE_NULL}.
265      *
266      * @throws IllegalStateException if the bitmap is not loaded.
267      * @see #isLoaded()
268      * @see #load(Context)
269      */
getBitmap()270     public Bitmap getBitmap() {
271         throwIfIconIsNotLoaded();
272         return mBitmap;
273     }
274 
275     /**
276      * Gets the X offset of the pointer icon hotspot.
277      *
278      * @return The hotspot X offset.
279      *
280      * @throws IllegalStateException if the bitmap is not loaded.
281      * @see #isLoaded()
282      * @see #load(Context)
283      */
getHotSpotX()284     public float getHotSpotX() {
285         throwIfIconIsNotLoaded();
286         return mHotSpotX;
287     }
288 
289     /**
290      * Gets the Y offset of the pointer icon hotspot.
291      *
292      * @return The hotspot Y offset.
293      *
294      * @throws IllegalStateException if the bitmap is not loaded.
295      * @see #isLoaded()
296      * @see #load(Context)
297      */
getHotSpotY()298     public float getHotSpotY() {
299         throwIfIconIsNotLoaded();
300         return mHotSpotY;
301     }
302 
throwIfIconIsNotLoaded()303     private void throwIfIconIsNotLoaded() {
304         if (!isLoaded()) {
305             throw new IllegalStateException("The icon is not loaded.");
306         }
307     }
308 
309     public static final Parcelable.Creator<PointerIcon> CREATOR
310             = new Parcelable.Creator<PointerIcon>() {
311         public PointerIcon createFromParcel(Parcel in) {
312             int style = in.readInt();
313             if (style == STYLE_NULL) {
314                 return getNullIcon();
315             }
316 
317             int systemIconResourceId = in.readInt();
318             if (systemIconResourceId != 0) {
319                 PointerIcon icon = new PointerIcon(style);
320                 icon.mSystemIconResourceId = systemIconResourceId;
321                 return icon;
322             }
323 
324             Bitmap bitmap = Bitmap.CREATOR.createFromParcel(in);
325             float hotSpotX = in.readFloat();
326             float hotSpotY = in.readFloat();
327             return PointerIcon.createCustomIcon(bitmap, hotSpotX, hotSpotY);
328         }
329 
330         public PointerIcon[] newArray(int size) {
331             return new PointerIcon[size];
332         }
333     };
334 
describeContents()335     public int describeContents() {
336         return 0;
337     }
338 
writeToParcel(Parcel out, int flags)339     public void writeToParcel(Parcel out, int flags) {
340         out.writeInt(mStyle);
341 
342         if (mStyle != STYLE_NULL) {
343             out.writeInt(mSystemIconResourceId);
344             if (mSystemIconResourceId == 0) {
345                 mBitmap.writeToParcel(out, flags);
346                 out.writeFloat(mHotSpotX);
347                 out.writeFloat(mHotSpotY);
348             }
349         }
350     }
351 
352     @Override
equals(Object other)353     public boolean equals(Object other) {
354         if (this == other) {
355             return true;
356         }
357 
358         if (other == null || !(other instanceof PointerIcon)) {
359             return false;
360         }
361 
362         PointerIcon otherIcon = (PointerIcon) other;
363         if (mStyle != otherIcon.mStyle
364                 || mSystemIconResourceId != otherIcon.mSystemIconResourceId) {
365             return false;
366         }
367 
368         if (mSystemIconResourceId == 0 && (mBitmap != otherIcon.mBitmap
369                 || mHotSpotX != otherIcon.mHotSpotX
370                 || mHotSpotY != otherIcon.mHotSpotY)) {
371             return false;
372         }
373 
374         return true;
375     }
376 
loadResource(Context context, Resources resources, @XmlRes int resourceId)377     private void loadResource(Context context, Resources resources, @XmlRes int resourceId) {
378         final XmlResourceParser parser = resources.getXml(resourceId);
379         final int bitmapRes;
380         final float hotSpotX;
381         final float hotSpotY;
382         try {
383             XmlUtils.beginDocument(parser, "pointer-icon");
384 
385             final TypedArray a = resources.obtainAttributes(
386                     parser, com.android.internal.R.styleable.PointerIcon);
387             bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
388             hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
389             hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
390             a.recycle();
391         } catch (Exception ex) {
392             throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
393         } finally {
394             parser.close();
395         }
396 
397         if (bitmapRes == 0) {
398             throw new IllegalArgumentException("<pointer-icon> is missing bitmap attribute.");
399         }
400 
401         Drawable drawable;
402         if (context == null) {
403             drawable = resources.getDrawable(bitmapRes);
404         } else {
405             drawable = context.getDrawable(bitmapRes);
406         }
407         if (!(drawable instanceof BitmapDrawable)) {
408             throw new IllegalArgumentException("<pointer-icon> bitmap attribute must "
409                     + "refer to a bitmap drawable.");
410         }
411 
412         // Set the properties now that we have successfully loaded the icon.
413         mBitmap = ((BitmapDrawable)drawable).getBitmap();
414         mHotSpotX = hotSpotX;
415         mHotSpotY = hotSpotY;
416     }
417 
validateHotSpot(Bitmap bitmap, float hotSpotX, float hotSpotY)418     private static void validateHotSpot(Bitmap bitmap, float hotSpotX, float hotSpotY) {
419         if (hotSpotX < 0 || hotSpotX >= bitmap.getWidth()) {
420             throw new IllegalArgumentException("x hotspot lies outside of the bitmap area");
421         }
422         if (hotSpotY < 0 || hotSpotY >= bitmap.getHeight()) {
423             throw new IllegalArgumentException("y hotspot lies outside of the bitmap area");
424         }
425     }
426 
getSystemIconStyleIndex(int style)427     private static int getSystemIconStyleIndex(int style) {
428         switch (style) {
429             case STYLE_ARROW:
430                 return com.android.internal.R.styleable.Pointer_pointerIconArrow;
431             case STYLE_SPOT_HOVER:
432                 return com.android.internal.R.styleable.Pointer_pointerIconSpotHover;
433             case STYLE_SPOT_TOUCH:
434                 return com.android.internal.R.styleable.Pointer_pointerIconSpotTouch;
435             case STYLE_SPOT_ANCHOR:
436                 return com.android.internal.R.styleable.Pointer_pointerIconSpotAnchor;
437             default:
438                 return 0;
439         }
440     }
441 }
442