1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
4 import static android.os.Build.VERSION_CODES.M;
5 import static android.os.Build.VERSION_CODES.N_MR1;
6 import static android.os.Build.VERSION_CODES.O;
7 import static android.os.Build.VERSION_CODES.P;
8 import static android.os.Build.VERSION_CODES.Q;
9 import static android.os.Build.VERSION_CODES.R;
10 import static android.os.Build.VERSION_CODES.S;
11 
12 import android.graphics.Bitmap;
13 import android.graphics.Canvas;
14 import android.graphics.ColorFilter;
15 import android.graphics.Matrix;
16 import android.graphics.Paint;
17 import android.graphics.Path;
18 import android.graphics.Rect;
19 import android.graphics.RectF;
20 import com.google.common.base.Preconditions;
21 import java.util.ArrayList;
22 import java.util.List;
23 import org.robolectric.annotation.Implementation;
24 import org.robolectric.annotation.Implements;
25 import org.robolectric.annotation.RealObject;
26 import org.robolectric.annotation.ReflectorObject;
27 import org.robolectric.annotation.Resetter;
28 import org.robolectric.res.android.NativeObjRegistry;
29 import org.robolectric.shadow.api.Shadow;
30 import org.robolectric.util.ReflectionHelpers;
31 import org.robolectric.util.reflector.Direct;
32 import org.robolectric.util.reflector.ForType;
33 
34 /**
35  * Broken. This implementation is very specific to the application for which it was developed. Todo:
36  * Reimplement. Consider using the same strategy of collecting a history of draw events and
37  * providing methods for writing queries based on type, number, and order of events.
38  */
39 @SuppressWarnings({"UnusedDeclaration"})
40 @Implements(value = Canvas.class, isInAndroidSdk = false)
41 public class ShadowLegacyCanvas extends ShadowCanvas {
42   private static final NativeObjRegistry<NativeCanvas> nativeObjectRegistry =
43       new NativeObjRegistry<>(NativeCanvas.class);
44 
45   @RealObject protected Canvas realCanvas;
46   @ReflectorObject protected CanvasReflector canvasReflector;
47 
48   private final List<RoundRectPaintHistoryEvent> roundRectPaintEvents = new ArrayList<>();
49   private List<PathPaintHistoryEvent> pathPaintEvents = new ArrayList<>();
50   private List<CirclePaintHistoryEvent> circlePaintEvents = new ArrayList<>();
51   private List<ArcPaintHistoryEvent> arcPaintEvents = new ArrayList<>();
52   private List<RectPaintHistoryEvent> rectPaintEvents = new ArrayList<>();
53   private List<LinePaintHistoryEvent> linePaintEvents = new ArrayList<>();
54   private List<OvalPaintHistoryEvent> ovalPaintEvents = new ArrayList<>();
55   private List<TextHistoryEvent> drawnTextEventHistory = new ArrayList<>();
56   private Paint drawnPaint;
57   private Bitmap targetBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
58   private float translateX;
59   private float translateY;
60   private float scaleX = 1;
61   private float scaleY = 1;
62   private int height;
63   private int width;
64 
65   @Implementation
__constructor__(Bitmap bitmap)66   protected void __constructor__(Bitmap bitmap) {
67     canvasReflector.__constructor__(bitmap);
68     this.targetBitmap = bitmap;
69   }
70 
getNativeId()71   private long getNativeId() {
72     return realCanvas.getNativeCanvasWrapper();
73   }
74 
getNativeCanvas()75   private NativeCanvas getNativeCanvas() {
76     return nativeObjectRegistry.getNativeObject(getNativeId());
77   }
78 
79   @Override
appendDescription(String s)80   public void appendDescription(String s) {
81     ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
82     shadowBitmap.appendDescription(s);
83   }
84 
85   @Override
getDescription()86   public String getDescription() {
87     ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
88     return shadowBitmap.getDescription();
89   }
90 
91   @Implementation
setBitmap(Bitmap bitmap)92   protected void setBitmap(Bitmap bitmap) {
93     targetBitmap = bitmap;
94   }
95 
96   @Implementation
drawText(String text, float x, float y, Paint paint)97   protected void drawText(String text, float x, float y, Paint paint) {
98     drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text));
99   }
100 
101   @Implementation
drawText(CharSequence text, int start, int end, float x, float y, Paint paint)102   protected void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) {
103     drawnTextEventHistory.add(
104         new TextHistoryEvent(x, y, paint, text.subSequence(start, end).toString()));
105   }
106 
107   @Implementation
drawText(char[] text, int index, int count, float x, float y, Paint paint)108   protected void drawText(char[] text, int index, int count, float x, float y, Paint paint) {
109     drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, new String(text, index, count)));
110   }
111 
112   @Implementation
drawText(String text, int start, int end, float x, float y, Paint paint)113   protected void drawText(String text, int start, int end, float x, float y, Paint paint) {
114     drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text.substring(start, end)));
115   }
116 
117   @Implementation
translate(float x, float y)118   protected void translate(float x, float y) {
119     this.translateX = x;
120     this.translateY = y;
121   }
122 
123   @Implementation
scale(float sx, float sy)124   protected void scale(float sx, float sy) {
125     this.scaleX = sx;
126     this.scaleY = sy;
127   }
128 
129   @Implementation
scale(float sx, float sy, float px, float py)130   protected void scale(float sx, float sy, float px, float py) {
131     this.scaleX = sx;
132     this.scaleY = sy;
133   }
134 
135   @Implementation
drawPaint(Paint paint)136   protected void drawPaint(Paint paint) {
137     drawnPaint = paint;
138   }
139 
140   @Implementation
drawColor(int color)141   protected void drawColor(int color) {
142     appendDescription("draw color " + color);
143   }
144 
145   @Implementation
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)146   protected void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
147     describeBitmap(bitmap, paint);
148 
149     int x = (int) (left + translateX);
150     int y = (int) (top + translateY);
151     if (x != 0 || y != 0) {
152       appendDescription(" at (" + x + "," + y + ")");
153     }
154 
155     if (scaleX != 1 && scaleY != 1) {
156       appendDescription(" scaled by (" + scaleX + "," + scaleY + ")");
157     }
158 
159     if (bitmap != null && targetBitmap != null) {
160       ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
161       shadowTargetBitmap.drawBitmap(bitmap, (int) left, (int) top);
162     }
163   }
164 
165   @Implementation
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)166   protected void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) {
167     describeBitmap(bitmap, paint);
168 
169     StringBuilder descriptionBuilder = new StringBuilder();
170     if (dst != null) {
171       descriptionBuilder
172           .append(" at (")
173           .append(dst.left)
174           .append(",")
175           .append(dst.top)
176           .append(") with height=")
177           .append(dst.height())
178           .append(" and width=")
179           .append(dst.width());
180     }
181 
182     if (src != null) {
183       descriptionBuilder.append(" taken from ").append(src.toString());
184     }
185     appendDescription(descriptionBuilder.toString());
186   }
187 
188   @Implementation
drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)189   protected void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) {
190     describeBitmap(bitmap, paint);
191 
192     StringBuilder descriptionBuilder = new StringBuilder();
193     if (dst != null) {
194       descriptionBuilder
195           .append(" at (")
196           .append(dst.left)
197           .append(",")
198           .append(dst.top)
199           .append(") with height=")
200           .append(dst.height())
201           .append(" and width=")
202           .append(dst.width());
203     }
204 
205     if (src != null) {
206       descriptionBuilder.append(" taken from ").append(src.toString());
207     }
208     appendDescription(descriptionBuilder.toString());
209   }
210 
211   @Implementation
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)212   protected void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) {
213     describeBitmap(bitmap, paint);
214 
215     ShadowMatrix shadowMatrix = Shadow.extract(matrix);
216     appendDescription(" transformed by " + shadowMatrix.getDescription());
217   }
218 
219   @Implementation
drawPath(Path path, Paint paint)220   protected void drawPath(Path path, Paint paint) {
221     pathPaintEvents.add(new PathPaintHistoryEvent(new Path(path), new Paint(paint)));
222 
223     separateLines();
224     ShadowPath shadowPath = Shadow.extract(path);
225     appendDescription("Path " + shadowPath.getPoints().toString());
226   }
227 
228   @Implementation
drawCircle(float cx, float cy, float radius, Paint paint)229   protected void drawCircle(float cx, float cy, float radius, Paint paint) {
230     circlePaintEvents.add(new CirclePaintHistoryEvent(cx, cy, radius, paint));
231   }
232 
233   @Implementation
drawArc( RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)234   protected void drawArc(
235       RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) {
236     arcPaintEvents.add(new ArcPaintHistoryEvent(oval, startAngle, sweepAngle, useCenter, paint));
237   }
238 
239   @Implementation
drawRect(float left, float top, float right, float bottom, Paint paint)240   protected void drawRect(float left, float top, float right, float bottom, Paint paint) {
241     rectPaintEvents.add(new RectPaintHistoryEvent(left, top, right, bottom, paint));
242 
243     if (targetBitmap != null) {
244       ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
245       shadowTargetBitmap.drawRect(new RectF(left, top, right, bottom), paint);
246     }
247   }
248 
249   @Implementation
drawRect(Rect r, Paint paint)250   protected void drawRect(Rect r, Paint paint) {
251     rectPaintEvents.add(new RectPaintHistoryEvent(r.left, r.top, r.right, r.bottom, paint));
252 
253     if (targetBitmap != null) {
254       ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
255       shadowTargetBitmap.drawRect(r, paint);
256     }
257   }
258 
259   @Implementation
drawRoundRect(RectF rect, float rx, float ry, Paint paint)260   protected void drawRoundRect(RectF rect, float rx, float ry, Paint paint) {
261     roundRectPaintEvents.add(
262         new RoundRectPaintHistoryEvent(
263             rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint));
264   }
265 
266   @Implementation
drawLine(float startX, float startY, float stopX, float stopY, Paint paint)267   protected void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
268     linePaintEvents.add(new LinePaintHistoryEvent(startX, startY, stopX, stopY, paint));
269   }
270 
271   @Implementation
drawOval(RectF oval, Paint paint)272   protected void drawOval(RectF oval, Paint paint) {
273     ovalPaintEvents.add(new OvalPaintHistoryEvent(oval, paint));
274   }
275 
describeBitmap(Bitmap bitmap, Paint paint)276   private void describeBitmap(Bitmap bitmap, Paint paint) {
277     separateLines();
278 
279     ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
280     appendDescription(shadowBitmap.getDescription());
281 
282     if (paint != null) {
283       ColorFilter colorFilter = paint.getColorFilter();
284       if (colorFilter != null) {
285         appendDescription(" with " + colorFilter.getClass().getSimpleName());
286       }
287     }
288   }
289 
separateLines()290   private void separateLines() {
291     if (getDescription().length() != 0) {
292       appendDescription("\n");
293     }
294   }
295 
296   @Override
getPathPaintHistoryCount()297   public int getPathPaintHistoryCount() {
298     return pathPaintEvents.size();
299   }
300 
301   @Override
getCirclePaintHistoryCount()302   public int getCirclePaintHistoryCount() {
303     return circlePaintEvents.size();
304   }
305 
306   @Override
getArcPaintHistoryCount()307   public int getArcPaintHistoryCount() {
308     return arcPaintEvents.size();
309   }
310 
311   @Override
hasDrawnPath()312   public boolean hasDrawnPath() {
313     return getPathPaintHistoryCount() > 0;
314   }
315 
316   @Override
hasDrawnCircle()317   public boolean hasDrawnCircle() {
318     return circlePaintEvents.size() > 0;
319   }
320 
321   @Override
getDrawnPathPaint(int i)322   public Paint getDrawnPathPaint(int i) {
323     return pathPaintEvents.get(i).pathPaint;
324   }
325 
326   @Override
getDrawnPath(int i)327   public Path getDrawnPath(int i) {
328     return pathPaintEvents.get(i).drawnPath;
329   }
330 
331   @Override
getDrawnCircle(int i)332   public CirclePaintHistoryEvent getDrawnCircle(int i) {
333     return circlePaintEvents.get(i);
334   }
335 
336   @Override
getDrawnArc(int i)337   public ArcPaintHistoryEvent getDrawnArc(int i) {
338     return arcPaintEvents.get(i);
339   }
340 
341   @Override
resetCanvasHistory()342   public void resetCanvasHistory() {
343     drawnTextEventHistory.clear();
344     pathPaintEvents.clear();
345     circlePaintEvents.clear();
346     rectPaintEvents.clear();
347     roundRectPaintEvents.clear();
348     linePaintEvents.clear();
349     ovalPaintEvents.clear();
350     ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
351     shadowBitmap.setDescription("");
352   }
353 
354   @Override
getDrawnPaint()355   public Paint getDrawnPaint() {
356     return drawnPaint;
357   }
358 
359   @Override
setHeight(int height)360   public void setHeight(int height) {
361     this.height = height;
362   }
363 
364   @Override
setWidth(int width)365   public void setWidth(int width) {
366     this.width = width;
367   }
368 
369   @Implementation
getWidth()370   protected int getWidth() {
371     if (width == 0) {
372       return targetBitmap.getWidth();
373     }
374     return width;
375   }
376 
377   @Implementation
getHeight()378   protected int getHeight() {
379     if (height == 0) {
380       return targetBitmap.getHeight();
381     }
382     return height;
383   }
384 
385   @Implementation
getClipBounds(Rect bounds)386   protected boolean getClipBounds(Rect bounds) {
387     Preconditions.checkNotNull(bounds);
388     if (targetBitmap == null) {
389       return false;
390     }
391     bounds.set(0, 0, targetBitmap.getWidth(), targetBitmap.getHeight());
392     return !bounds.isEmpty();
393   }
394 
395   @Override
getDrawnTextEvent(int i)396   public TextHistoryEvent getDrawnTextEvent(int i) {
397     return drawnTextEventHistory.get(i);
398   }
399 
400   @Override
getTextHistoryCount()401   public int getTextHistoryCount() {
402     return drawnTextEventHistory.size();
403   }
404 
405   @Override
getDrawnRect(int i)406   public RectPaintHistoryEvent getDrawnRect(int i) {
407     return rectPaintEvents.get(i);
408   }
409 
410   @Override
getLastDrawnRect()411   public RectPaintHistoryEvent getLastDrawnRect() {
412     return rectPaintEvents.get(rectPaintEvents.size() - 1);
413   }
414 
415   @Override
getRectPaintHistoryCount()416   public int getRectPaintHistoryCount() {
417     return rectPaintEvents.size();
418   }
419 
420   @Override
getDrawnRoundRect(int i)421   public RoundRectPaintHistoryEvent getDrawnRoundRect(int i) {
422     return roundRectPaintEvents.get(i);
423   }
424 
425   @Override
getLastDrawnRoundRect()426   public RoundRectPaintHistoryEvent getLastDrawnRoundRect() {
427     return roundRectPaintEvents.get(roundRectPaintEvents.size() - 1);
428   }
429 
430   @Override
getRoundRectPaintHistoryCount()431   public int getRoundRectPaintHistoryCount() {
432     return roundRectPaintEvents.size();
433   }
434 
435   @Override
getDrawnLine(int i)436   public LinePaintHistoryEvent getDrawnLine(int i) {
437     return linePaintEvents.get(i);
438   }
439 
440   @Override
getLinePaintHistoryCount()441   public int getLinePaintHistoryCount() {
442     return linePaintEvents.size();
443   }
444 
445   @Override
getOvalPaintHistoryCount()446   public int getOvalPaintHistoryCount() {
447     return ovalPaintEvents.size();
448   }
449 
450   @Override
getDrawnOval(int i)451   public OvalPaintHistoryEvent getDrawnOval(int i) {
452     return ovalPaintEvents.get(i);
453   }
454 
455   @Implementation(maxSdk = N_MR1)
save()456   protected int save() {
457     return getNativeCanvas().save();
458   }
459 
460   @Implementation(maxSdk = N_MR1)
restore()461   protected void restore() {
462     getNativeCanvas().restore();
463   }
464 
465   @Implementation(maxSdk = N_MR1)
getSaveCount()466   protected int getSaveCount() {
467     return getNativeCanvas().getSaveCount();
468   }
469 
470   @Implementation(maxSdk = N_MR1)
restoreToCount(int saveCount)471   protected void restoreToCount(int saveCount) {
472     getNativeCanvas().restoreToCount(saveCount);
473   }
474 
475   @Implementation
release()476   protected void release() {
477     nativeObjectRegistry.unregister(getNativeId());
478     canvasReflector.release();
479   }
480 
481   @Implementation(maxSdk = LOLLIPOP_MR1)
initRaster(long bitmapHandle)482   protected static long initRaster(long bitmapHandle) {
483     return nativeObjectRegistry.register(new NativeCanvas());
484   }
485 
486   @Implementation(minSdk = M, maxSdk = N_MR1)
initRaster(Bitmap bitmap)487   protected static long initRaster(Bitmap bitmap) {
488     return nativeObjectRegistry.register(new NativeCanvas());
489   }
490 
491   @Implementation(minSdk = O, maxSdk = P)
nInitRaster(Bitmap bitmap)492   protected static long nInitRaster(Bitmap bitmap) {
493     return nativeObjectRegistry.register(new NativeCanvas());
494   }
495 
496   @Implementation(minSdk = Q)
nInitRaster(long bitmapHandle)497   protected static long nInitRaster(long bitmapHandle) {
498     return nativeObjectRegistry.register(new NativeCanvas());
499   }
500 
501   @Implementation(minSdk = O)
nGetSaveCount(long canvasHandle)502   protected static int nGetSaveCount(long canvasHandle) {
503     return nativeObjectRegistry.getNativeObject(canvasHandle).getSaveCount();
504   }
505 
506   @Implementation(minSdk = O)
nSave(long canvasHandle, int saveFlags)507   protected static int nSave(long canvasHandle, int saveFlags) {
508     return nativeObjectRegistry.getNativeObject(canvasHandle).save();
509   }
510 
511   @Implementation(maxSdk = N_MR1)
native_saveLayer( long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags)512   protected static int native_saveLayer(
513       long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
514     return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
515   }
516 
517   @Implementation(minSdk = O, maxSdk = R)
nSaveLayer( long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags)518   protected static int nSaveLayer(
519       long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
520     return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
521   }
522 
523   @Implementation(minSdk = S)
nSaveLayer( long nativeCanvas, float l, float t, float r, float b, long nativePaint)524   protected static int nSaveLayer(
525       long nativeCanvas, float l, float t, float r, float b, long nativePaint) {
526     return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
527   }
528 
529   @Implementation(maxSdk = N_MR1)
native_saveLayerAlpha( long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags)530   protected static int native_saveLayerAlpha(
531       long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
532     return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
533   }
534 
535   @Implementation(minSdk = O, maxSdk = R)
nSaveLayerAlpha( long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags)536   protected static int nSaveLayerAlpha(
537       long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
538     return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
539   }
540 
541   @Implementation(minSdk = S)
nSaveLayerAlpha( long nativeCanvas, float l, float t, float r, float b, int alpha)542   protected static int nSaveLayerAlpha(
543       long nativeCanvas, float l, float t, float r, float b, int alpha) {
544     return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
545   }
546 
547   @Implementation(minSdk = O)
nRestore(long canvasHandle)548   protected static boolean nRestore(long canvasHandle) {
549     return nativeObjectRegistry.getNativeObject(canvasHandle).restore();
550   }
551 
552   @Implementation(minSdk = O)
nRestoreToCount(long canvasHandle, int saveCount)553   protected static void nRestoreToCount(long canvasHandle, int saveCount) {
554     nativeObjectRegistry.getNativeObject(canvasHandle).restoreToCount(saveCount);
555   }
556 
557   private static class PathPaintHistoryEvent {
558     private final Path drawnPath;
559     private final Paint pathPaint;
560 
PathPaintHistoryEvent(Path drawnPath, Paint pathPaint)561     PathPaintHistoryEvent(Path drawnPath, Paint pathPaint) {
562       this.drawnPath = drawnPath;
563       this.pathPaint = pathPaint;
564     }
565   }
566 
567   @Resetter
reset()568   public static void reset() {
569     nativeObjectRegistry.clear();
570   }
571 
572   @SuppressWarnings("MemberName")
573   @ForType(Canvas.class)
574   private interface CanvasReflector {
575     @Direct
__constructor__(Bitmap bitmap)576     void __constructor__(Bitmap bitmap);
577 
578     @Direct
release()579     void release();
580   }
581 
582   private static class NativeCanvas {
583     private int saveCount = 1;
584 
save()585     int save() {
586       return saveCount++;
587     }
588 
restore()589     boolean restore() {
590       if (saveCount > 1) {
591         saveCount--;
592         return true;
593       } else {
594         return false;
595       }
596     }
597 
getSaveCount()598     int getSaveCount() {
599       return saveCount;
600     }
601 
restoreToCount(int saveCount)602     void restoreToCount(int saveCount) {
603       if (saveCount > 0) {
604         this.saveCount = saveCount;
605       }
606     }
607   }
608 }
609