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