1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.KITKAT_WATCH; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP; 5 import static org.robolectric.RuntimeEnvironment.getApiLevel; 6 import static org.robolectric.shadow.api.Shadow.directlyOn; 7 import static org.robolectric.util.ReflectionHelpers.getField; 8 import static org.robolectric.util.ReflectionHelpers.getStaticField; 9 import static org.robolectric.util.ReflectionHelpers.setField; 10 import static org.robolectric.util.ReflectionHelpers.setStaticField; 11 12 import android.os.Handler; 13 import android.os.Looper; 14 import android.os.Message; 15 import android.os.MessageQueue; 16 import org.robolectric.annotation.HiddenApi; 17 import org.robolectric.annotation.Implementation; 18 import org.robolectric.annotation.Implements; 19 import org.robolectric.annotation.RealObject; 20 import org.robolectric.annotation.Resetter; 21 import org.robolectric.shadow.api.Shadow; 22 23 @Implements(Message.class) 24 public class ShadowMessage { 25 @RealObject 26 private Message realMessage; 27 private Runnable scheduledRunnable; 28 29 private static final Object lock = getStaticField(Message.class, "sPoolSync"); 30 unschedule()31 private void unschedule() { 32 Handler target = realMessage.getTarget(); 33 34 if (target != null && scheduledRunnable != null) { 35 shadowOf(target.getLooper()).getScheduler().remove(scheduledRunnable); 36 scheduledRunnable = null; 37 } 38 } 39 40 /** 41 * Hook to unscheduled the callback when the message is recycled. 42 * Invokes {@link #unschedule()} and then calls through to the 43 * package private method {@link Message#recycleUnchecked()} 44 * on the real object. 45 */ 46 @HiddenApi 47 @Implementation(minSdk = LOLLIPOP) recycleUnchecked()48 public void recycleUnchecked() { 49 if (getApiLevel() >= LOLLIPOP) { 50 unschedule(); 51 directlyOn(realMessage, Message.class, "recycleUnchecked"); 52 } else { 53 // provide forward compatibility with SDK 21. 54 recycle(); 55 } 56 } 57 58 /** 59 * Hook to unscheduled the callback when the message is recycled. Invokes {@link #unschedule()} 60 * and then calls through to {@link Message#recycle()} on the real object. 61 */ 62 @Implementation(maxSdk = KITKAT_WATCH) recycle()63 protected void recycle() { 64 unschedule(); 65 directlyOn(realMessage, Message.class, "recycle"); 66 } 67 68 /** 69 * Stores the {@link Runnable} instance that has been scheduled 70 * to invoke this message. This is called when the message is 71 * enqueued by {@link ShadowMessageQueue#enqueueMessage} and is used when 72 * the message is recycled to ensure that the correct 73 * {@link Runnable} instance is removed from the associated scheduler. 74 * 75 * @param r the {@link Runnable} instance that is scheduled to 76 * trigger this message. 77 * 78 #if ($api >= 21) * @see #recycleUnchecked() 79 #else * @see #recycle() 80 #end 81 */ setScheduledRunnable(Runnable r)82 public void setScheduledRunnable(Runnable r) { 83 scheduledRunnable = r; 84 } 85 86 /** 87 * Convenience method to provide access to the private {@code Message.isInUse()} method. Note that 88 * the definition of "in use" changed with API 21: 89 * 90 * <p>In API 19, a message was only considered "in use" during its dispatch. In API 21, the 91 * message is considered "in use" from the time it is enqueued until the time that it is freshly 92 * obtained via a call to {@link Message#obtain()}. This means that in API 21 messages that are in 93 * the recycled pool will still be marked as "in use". 94 * 95 * @return {@code true} if the message is currently "in use", {@code false} otherwise. 96 */ 97 @Implementation isInUse()98 protected boolean isInUse() { 99 return directlyOn(realMessage, Message.class, "isInUse"); 100 } 101 102 /** 103 * Convenience method to provide getter access to the private field 104 * {@code Message.next}. 105 * 106 * @return The next message in the current message chain. 107 * @see #setNext(Message) 108 */ getNext()109 public Message getNext() { 110 return getField(realMessage, "next"); 111 } 112 113 /** 114 * Convenience method to provide setter access to the private field 115 * {@code Message.next}. 116 * 117 * @param next the new next message for the current message. 118 * @see #getNext() 119 */ setNext(Message next)120 public void setNext(Message next) { 121 setField(realMessage, "next", next); 122 } 123 124 /** 125 * Resets the static state of the {@link Message} class by 126 * emptying the message pool. 127 */ 128 @Resetter reset()129 public static void reset() { 130 synchronized (lock) { 131 setStaticField(Message.class, "sPoolSize", 0); 132 setStaticField(Message.class, "sPool", null); 133 } 134 } 135 shadowOf(Looper looper)136 private static ShadowLooper shadowOf(Looper looper) { 137 return Shadow.extract(looper); 138 } 139 shadowOf(MessageQueue mq)140 private static ShadowMessageQueue shadowOf(MessageQueue mq) { 141 return Shadow.extract(mq); 142 } 143 } 144