Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weirdest Android bug ever - Maybe a ProGuard issue?

I don't even know what title to give this, it's so strange. I built a little Android logic puzzle that uses color values in an ARGB-integer format. To blend the colors for the animation when you complete a level, I have the following function:

public static int blend(int color1, int color2, double position) {
    if (position<0) position=0;
    if (position>1) position=1;
    int a = (color1 >>> 24) & 0xFF; 
    int r = (color1 >>> 16) & 0xFF; 
    int g = (color1 >>>  8) & 0xFF; 
    int b =  color1         & 0xFF;

    int da = ((color2 >>> 24) & 0xFF) - a; 
    int dr = ((color2 >>> 16) & 0xFF) - r; 
    int dg = ((color2 >>>  8) & 0xFF) - g; 
    int db = ( color2         & 0xFF) - b;

    a += da * position;
    r += dr * position;
    g += dg * position;
    b += db * position;

    return (a<<24) | (r<<16) | (g<<8) | b;
}

I call this function during the animation with this code (includes debug print statement):

int color = blend(START_COLOR, END_COLOR, pos*pos*pos*pos*pos);
System.out.println(Integer.toHexString(START_COLOR)+", "+Integer.toHexString(END_COLOR)+", "+pos+" -> "+Integer.toHexString(color));

Here, pos is simply a double-value that counts from 0.0 to 1.0.

If I run this code directly on my phone from within Eclipse via the Android developer plugin, everything works just fine.

BUT: If I package the app and install the APK, it reliably screws up, giving me output similar to this:

...
fff9b233, f785a307, 0.877 -> fabcaa1c
fff9b233, f785a307, 0.881 -> fabbaa1b
fff9b233, f785a307, 0.883 -> fabaa91b
fff9b233, f785a307, 0.886 -> fab9a91a
fff9b233, f785a307, 0.89 -> fab8a91a
fff9b233, f785a307, 0.891 -> fa00a91a
fff9b233, f785a307, 0.895 -> fab6a919
fff9b233, f785a307, 0.896 -> fa00a919
fff9b233, f785a307, 0.901 -> fab4a918
fff9b233, f785a307, 0.901 -> fab4a918
fff9b233, f785a307, 0.907 -> fab1a817
fff9b233, f785a307, 0.907 -> fab1a817
fff9b233, f785a307, 0.912 -> f9afa817
fff9b233, f785a307, 0.913 -> f900a817
fff9b233, f785a307, 0.919 -> f9aca816
fff9b233, f785a307, 0.919 -> f9aca816
fff9b233, f785a307, 0.925 -> f9aaa715
fff9b233, f785a307, 0.925 -> f9aaa715
fff9b233, f785a307, 0.93 -> f900a714
fff9b233, f785a307, 0.931 -> f900a714
fff9b233, f785a307, 0.936 -> f900a713
fff9b233, f785a307, 0.937 -> f900a713
fff9b233, f785a307, 0.942 -> f900a612
fff9b233, f785a307, 0.942 -> f900a612
fff9b233, f785a307, 0.947 -> f800a611
fff9b233, f785a307, 0.948 -> f800a611
fff9b233, f785a307, 0.954 -> f800a610
fff9b233, f785a307, 0.954 -> f800a610
fff9b233, f785a307, 0.959 -> f800a50f
...

In this example, up until position 0.89, everything is fine. Then, things start to oscillate between working and screwing up just the R-component (set to 0; it's always the R-component that gets screwed up), and eventually, starting at 0.93 in this example, things always screw up. And then, if I run the exact same animation again, it starts screwing up right away...

How on earth is this possible? Is this ProGuard messing with my code? If that's a possibility, is there a way to find out for sure? I'm really at a loss of ideas here... And how can it be probabilistic whether it works or not? Or am I just missing something totally obvious here?

If it could be a ProGuard issue, what kind of optimizations could affect this part of the code? Is there a list of switches I could try to turn off one-by-one to find the flaky one?

UPDATE:

My project.properties file looks like this (commented lines deleted):

proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
target=android-22

and proguard-project.txt like this:

-flattenpackagehierarchy
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable

proguard-android.txt in the SDK directory should still be just like it was shipped with the SDK Tools v24.1.2 (assuming that's the package that includes ProGuard...; again excluding comments):

-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
-dontoptimize
-dontpreverify

-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

-keepclasseswithmembernames class * {
    native <methods>;
}

-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

-dontwarn android.support.**

UPDATE 2:

I think I found the compiled output of what ProGuard does with the blend-method in the dump.txt file:

+ Method:       a(IID)I
  Access flags: 0x9
    = public static int a(int,int,double)
  Class member attributes (count = 1):
  + Code attribute instructions (code length = 171, locals = 12, stack = 6):
    [0] dload_2 v2
    [1] dconst_0
    [2] dcmpg
    [3] ifge +5 (target=8)
    [6] dconst_0
    [7] dstore_2 v2
    [8] dload_2 v2
    [9] dconst_1
    [10] dcmpl
    [11] ifle +5 (target=16)
    [14] dconst_1
    [15] dstore_2 v2
    [16] iload_0 v0
    [17] bipush 24
    [19] iushr
    [20] sipush 255
    [23] iand
    [24] istore v4
    [26] iload_0 v0
    [27] bipush 16
    [29] iushr
    [30] sipush 255
    [33] iand
    [34] istore v5
    [36] iload_0 v0
    [37] bipush 8
    [39] iushr
    [40] sipush 255
    [43] iand
    [44] istore v6
    [46] iload_0 v0
    [47] sipush 255
    [50] iand
    [51] istore v7
    [53] iload_1 v1
    [54] bipush 24
    [56] iushr
    [57] sipush 255
    [60] iand
    [61] iload v4
    [63] isub
    [64] istore v8
    [66] iload_1 v1
    [67] bipush 16
    [69] iushr
    [70] sipush 255
    [73] iand
    [74] iload v5
    [76] isub
    [77] istore v9
    [79] iload_1 v1
    [80] bipush 8
    [82] iushr
    [83] sipush 255
    [86] iand
    [87] iload v6
    [89] isub
    [90] istore v10
    [92] iload_1 v1
    [93] sipush 255
    [96] iand
    [97] iload v7
    [99] isub
    [100] istore v11
    [102] iload v4
    [104] i2d
    [105] iload v8
    [107] i2d
    [108] dload_2 v2
    [109] dmul
    [110] dadd
    [111] d2i
    [112] istore v4
    [114] iload v5
    [116] i2d
    [117] iload v9
    [119] i2d
    [120] dload_2 v2
    [121] dmul
    [122] dadd
    [123] d2i
    [124] istore v5
    [126] iload v6
    [128] i2d
    [129] iload v10
    [131] i2d
    [132] dload_2 v2
    [133] dmul
    [134] dadd
    [135] d2i
    [136] istore v6
    [138] iload v7
    [140] i2d
    [141] iload v11
    [143] i2d
    [144] dload_2 v2
    [145] dmul
    [146] dadd
    [147] d2i
    [148] istore v7
    [150] iload v4
    [152] bipush 24
    [154] ishl
    [155] iload v5
    [157] bipush 16
    [159] ishl
    [160] ior
    [161] iload v6
    [163] bipush 8
    [165] ishl
    [166] ior
    [167] iload v7
    [169] ior
    [170] ireturn
    Code attribute exceptions (count = 0):
    Code attribute attributes (attribute count = 2):
    + Line number table attribute (count = 15)
      [0] -> line 33
      [8] -> line 34
      [16] -> line 35
      [26] -> line 36
      [36] -> line 37
      [46] -> line 38
      [53] -> line 40
      [66] -> line 41
      [79] -> line 42
      [92] -> line 43
      [102] -> line 45
      [114] -> line 46
      [126] -> line 47
      [138] -> line 48
      [150] -> line 50
    + Stack map table attribute (count = 2):
      - [8] Var: ..., Stack: (empty)
      - [16] Var: ..., Stack: (empty)

UPDATE 3:

I tried to rewrite the blend method to this (idea being that if I treat all components the same, it shouldn't be possible to screw up just one any more):

public static int blend(int color1, int color2, double position) {
    if (position<0) position=0;
    if (position>1) position=1;

    int result = 0;

    for (int shift = 0; shift<32; shift += 8) {
        int component =  (color1 >>> shift) & 0xFF;
        int change    = ((color2 >>> shift) & 0xFF) - component;
        component += change * position;
        result |= component << shift;
    }
    return result;
}

Not surprisingly, this code works now, just as it should! But this still doesn't get me any closer to understanding why the original code failed and in what other places of my app something similarly trivial could fail in unexpected ways.

UPDATE 4:

Simply reordering the lines to this also fixes the issue:

public static int blend(int color1, int color2, double position) {
    if (position<0) position=0;
    if (position>1) position=1;

    int a  =  (color1 >>> 24) & 0xFF;
    int da = ((color2 >>> 24) & 0xFF) - a;
    a += da * position;

    int r  =  (color1 >>> 16) & 0xFF; 
    int dr = ((color2 >>> 16) & 0xFF) - r;
    r += dr * position;

    int g  =  (color1 >>>  8) & 0xFF; 
    int dg = ((color2 >>>  8) & 0xFF) - g;
    g += dg * position;

    int b  =   color1         & 0xFF;
    int db = ( color2         & 0xFF) - b;
    b += db * position;

    return (a<<24) | (r<<16) | (g<<8) | b;
}

It must be some local-variable-reuse thing, I just don't know why that isn't apparent from the dump.txt file above... Is this something that Dalvik does (but only to signed APKs!?!)?

like image 769
Markus A. Avatar asked Sep 11 '15 23:09

Markus A.


1 Answers

Really interesting issue to investigate and solving it will definitely increase your level of expertise with (possibly) ProGuard, but for the sake of looking at the bigger picture, I would recommend going with existing tools for animating color changes :)

ArgbEvaluator (or ValueAnimator#ofArgb(int...) for API 21+) to the rescue!

API 21+:

ValueAnimator animator = ValueAnimator.ofArgb(0xFFFF0000, 0xFF00FF00); //red->green

API 11+:

ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(), 0xFFFF0000, 0xFF00FF00); //red->green
animator.setDuration(1000);//1second
animator.start();

It allows you to tweak it as you need (different interpolators, delays, listeners, etc) and also since it is coming from the platform there is a big chance ProGuard won't touch it

PS. I still really wanna see the root cause for the issue you are experiencing :)

like image 107
Pavel Dudka Avatar answered Sep 20 '22 13:09

Pavel Dudka