Project

General

Profile

« Previous | Next » 

Revision 1cd323dd

Added by Leszek Koltunski about 3 years ago

Move yet more code to objectlib.
It's almost possible to move the PreRender to objectlib now.

View differences:

src/main/java/org/distorted/main/RubikObjectStateActioner.java
1
///////////////////////////////////////////////////////////////////////////////////////////////////
2
// Copyright 2019 Leszek Koltunski                                                               //
3
//                                                                                               //
4
// This file is part of Magic Cube.                                                              //
5
//                                                                                               //
6
// Magic Cube is free software: you can redistribute it and/or modify                            //
7
// it under the terms of the GNU General Public License as published by                          //
8
// the Free Software Foundation, either version 2 of the License, or                             //
9
// (at your option) any later version.                                                           //
10
//                                                                                               //
11
// Magic Cube is distributed in the hope that it will be useful,                                 //
12
// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
13
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
14
// GNU General Public License for more details.                                                  //
15
//                                                                                               //
16
// You should have received a copy of the GNU General Public License                             //
17
// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
18
///////////////////////////////////////////////////////////////////////////////////////////////////
19

  
20
package org.distorted.main;
21

  
22
import android.os.Bundle;
23

  
24
import androidx.annotation.NonNull;
25

  
26
import com.google.android.play.core.review.ReviewInfo;
27
import com.google.android.play.core.review.ReviewManager;
28
import com.google.android.play.core.review.ReviewManagerFactory;
29
import com.google.android.play.core.tasks.OnCompleteListener;
30
import com.google.android.play.core.tasks.OnFailureListener;
31
import com.google.android.play.core.tasks.Task;
32
import com.google.firebase.analytics.FirebaseAnalytics;
33

  
34
import org.distorted.objectlib.helpers.ObjectStateActioner;
35
import org.distorted.objectlib.helpers.TwistyActivity;
36
import org.distorted.objectlib.main.ObjectType;
37

  
38
import org.distorted.dialogs.RubikDialogNewRecord;
39
import org.distorted.dialogs.RubikDialogSolved;
40
import org.distorted.network.RubikScores;
41
import org.distorted.screens.RubikScreenPlay;
42
import org.distorted.screens.RubikScreenSolving;
43
import org.distorted.screens.ScreenList;
44

  
45
///////////////////////////////////////////////////////////////////////////////////////////////////
46

  
47
public class RubikObjectStateActioner implements ObjectStateActioner
48
{
49
  private boolean mIsNewRecord;
50
  private long mNewRecord;
51

  
52
///////////////////////////////////////////////////////////////////////////////////////////////////
53

  
54
  private void analyticsReport(TwistyActivity act, String message, String name, long timeBegin)
55
    {
56
    long elapsed = System.currentTimeMillis() - timeBegin;
57
    String msg = message+" startTime: "+timeBegin+" elapsed: "+elapsed+" name: "+name;
58

  
59
    if( BuildConfig.DEBUG )
60
       {
61
       android.util.Log.d("pre", msg);
62
       }
63
    else
64
      {
65
      RubikActivity ract = (RubikActivity)act;
66
      FirebaseAnalytics analytics = ract.getAnalytics();
67

  
68
      if( analytics!=null )
69
        {
70
        Bundle bundle = new Bundle();
71
        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, msg);
72
        analytics.logEvent(FirebaseAnalytics.Event.SHARE, bundle);
73
        }
74
      }
75
    }
76

  
77
///////////////////////////////////////////////////////////////////////////////////////////////////
78

  
79
  private void reportRecord(TwistyActivity act, String debug, int scrambleNum)
80
    {
81
    RubikActivity ract = (RubikActivity)act;
82
    RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
83
    RubikScores scores = RubikScores.getInstance();
84

  
85
    int object      = play.getObject();
86
    int level       = play.getLevel();
87
    ObjectType list = ObjectType.getObject(object);
88
    String name     = scores.getName();
89

  
90
    String record = list.name()+" level "+level+" time "+mNewRecord+" isNew: "+mIsNewRecord+" scrambleNum: "+scrambleNum;
91

  
92
    if( BuildConfig.DEBUG )
93
       {
94
       android.util.Log.e("pre", debug);
95
       android.util.Log.e("pre", name);
96
       android.util.Log.e("pre", record);
97
       }
98
    else
99
      {
100
      FirebaseAnalytics analytics = ract.getAnalytics();
101

  
102
      if( analytics!=null )
103
        {
104
        Bundle bundle = new Bundle();
105
        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, debug);
106
        bundle.putString(FirebaseAnalytics.Param.CHARACTER, name);
107
        bundle.putString(FirebaseAnalytics.Param.LEVEL, record);
108
        analytics.logEvent(FirebaseAnalytics.Event.LEVEL_UP, bundle);
109
        }
110
      }
111
    }
112

  
113
///////////////////////////////////////////////////////////////////////////////////////////////////
114

  
115
  private void requestReview(TwistyActivity act)
116
    {
117
    final RubikScores scores = RubikScores.getInstance();
118
    int numWins = scores.incrementNumWins();
119

  
120
    if( numWins==7 || numWins==30 || numWins==100 || numWins==200)
121
      {
122
      final long timeBegin = System.currentTimeMillis();
123
      final ReviewManager manager = ReviewManagerFactory.create(act);
124
      Task<ReviewInfo> request = manager.requestReviewFlow();
125

  
126
      request.addOnCompleteListener(new OnCompleteListener<ReviewInfo>()
127
        {
128
        @Override
129
        public void onComplete (@NonNull Task<ReviewInfo> task)
130
          {
131
          if (task.isSuccessful())
132
            {
133
            final String name = scores.getName();
134
            ReviewInfo reviewInfo = task.getResult();
135
            Task<Void> flow = manager.launchReviewFlow(act, reviewInfo);
136

  
137
            flow.addOnFailureListener(new OnFailureListener()
138
              {
139
              @Override
140
              public void onFailure(Exception e)
141
                {
142
                analyticsReport(act,"Failed", name, timeBegin);
143
                }
144
              });
145

  
146
            flow.addOnCompleteListener(new OnCompleteListener<Void>()
147
              {
148
              @Override
149
              public void onComplete(@NonNull Task<Void> task)
150
                {
151
                analyticsReport(act,"Complete", name, timeBegin);
152
                }
153
              });
154
            }
155
          else
156
            {
157
            String name = scores.getName();
158
            analyticsReport(act,"Not Successful", name, timeBegin);
159
            }
160
          }
161
        });
162
      }
163
    }
164

  
165
///////////////////////////////////////////////////////////////////////////////////////////////////
166

  
167
   public void onWinEffectFinished(TwistyActivity act, String debug, int scrambleNum)
168
     {
169
     if( ScreenList.getCurrentScreen()== ScreenList.SOLV )
170
       {
171
       RubikActivity ract = (RubikActivity)act;
172
       Bundle bundle = new Bundle();
173
       bundle.putLong("time", mNewRecord );
174

  
175
       reportRecord(act,debug,scrambleNum);
176
       requestReview(act);
177

  
178
       if( mIsNewRecord )
179
         {
180
         RubikDialogNewRecord dialog = new RubikDialogNewRecord();
181
         dialog.setArguments(bundle);
182
         dialog.show( act.getSupportFragmentManager(), RubikDialogNewRecord.getDialogTag() );
183
         }
184
       else
185
         {
186
         RubikDialogSolved dialog = new RubikDialogSolved();
187
         dialog.setArguments(bundle);
188
         dialog.show( act.getSupportFragmentManager(), RubikDialogSolved.getDialogTag() );
189
         }
190

  
191
       act.runOnUiThread(new Runnable()
192
         {
193
         @Override
194
         public void run()
195
           {
196
           ScreenList.switchScreen( ract, ScreenList.DONE);
197
           }
198
         });
199
       }
200
     }
201

  
202
///////////////////////////////////////////////////////////////////////////////////////////////////
203

  
204
   public void onScrambleEffectFinished(TwistyActivity act)
205
     {
206
     RubikActivity ract = (RubikActivity)act;
207

  
208
     RubikScores.getInstance().incrementNumPlays();
209

  
210
     act.runOnUiThread(new Runnable()
211
       {
212
       @Override
213
       public void run()
214
         {
215
         ScreenList.switchScreen( ract, ScreenList.READ);
216
         }
217
       });
218
     }
219

  
220
///////////////////////////////////////////////////////////////////////////////////////////////////
221

  
222
   public void onSolved()
223
     {
224
     if( ScreenList.getCurrentScreen()== ScreenList.SOLV )
225
        {
226
        RubikScreenSolving solving = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
227
        mNewRecord = solving.getRecord();
228

  
229
        if( mNewRecord< 0 )
230
          {
231
          mNewRecord = -mNewRecord;
232
          mIsNewRecord = false;
233
          }
234
        else
235
          {
236
          mIsNewRecord = true;
237
          }
238
        }
239
     }
240
}
src/main/java/org/distorted/main/RubikPreRender.java
22 22
import android.content.Context;
23 23
import android.content.SharedPreferences;
24 24
import android.content.res.Resources;
25
import android.os.Bundle;
26

  
27
import androidx.annotation.NonNull;
28

  
29
import com.google.android.play.core.review.ReviewInfo;
30
import com.google.android.play.core.review.ReviewManager;
31
import com.google.android.play.core.review.ReviewManagerFactory;
32
import com.google.android.play.core.tasks.OnCompleteListener;
33
import com.google.android.play.core.tasks.OnFailureListener;
34
import com.google.android.play.core.tasks.Task;
35
import com.google.firebase.analytics.FirebaseAnalytics;
36 25

  
26
import org.distorted.objectlib.helpers.ObjectStateActioner;
37 27
import org.distorted.objectlib.main.TwistyObject;
38 28
import org.distorted.objectlib.main.ObjectType;
39

  
40
import org.distorted.dialogs.RubikDialogNewRecord;
41
import org.distorted.dialogs.RubikDialogSolved;
42 29
import org.distorted.objectlib.effects.BaseEffect;
43 30
import org.distorted.objectlib.effects.EffectController;
44 31
import org.distorted.objectlib.effects.scramble.ScrambleEffect;
45 32
import org.distorted.objectlib.helpers.BlockController;
46 33
import org.distorted.objectlib.helpers.MovesFinished;
47 34
import org.distorted.objectlib.helpers.TwistyPreRender;
48
import org.distorted.network.RubikScores;
49
import org.distorted.screens.RubikScreenPlay;
50
import org.distorted.screens.ScreenList;
51
import org.distorted.screens.RubikScreenSolving;
52 35

  
53 36
///////////////////////////////////////////////////////////////////////////////////////////////////
54 37

  
......
63 46
  private ObjectType mNextObject;
64 47
  private long mRotationFinishedID;
65 48
  private final long[] mEffectID;
66
  private boolean mIsNewRecord;
67
  private long mNewRecord;
68 49
  private int mScreenWidth;
69 50
  private SharedPreferences mPreferences;
70 51
  private int[][] mNextMoves;
......
76 57
  private long mAddRotationID, mRemoveRotationID;
77 58
  private int mCubit, mFace, mNewColor;
78 59
  private int mNearestAngle;
79
  private String mDebug;
80 60
  private long mDebugStartTime;
81 61
  private final BlockController mBlockController;
62
  private final ObjectStateActioner mActioner;
63
  private String mDebug;
82 64

  
83 65
///////////////////////////////////////////////////////////////////////////////////////////////////
84 66

  
85
  RubikPreRender(RubikSurfaceView view)
67
  RubikPreRender(RubikSurfaceView view, ObjectStateActioner actioner)
86 68
    {
87 69
    mView = view;
70
    mActioner = actioner;
88 71

  
89 72
    mFinishRotation       = false;
90 73
    mRemoveRotation       = false;
......
100 83
    mOldObject = null;
101 84
    mNewObject = null;
102 85

  
86
    mDebug = "";
87

  
103 88
    mScreenWidth = 0;
104 89
    mScrambleObjectNum = 0;
105 90

  
106 91
    mEffectID = new long[BaseEffect.Type.LENGTH];
107 92

  
108
    mDebug = "";
109

  
110 93
    RubikActivity act = (RubikActivity)mView.getContext();
111 94
    mBlockController = new BlockController(act);
112 95
    unblockEverything();
......
162 145

  
163 146
    if( solved && !mIsSolved )
164 147
      {
165
      if( ScreenList.getCurrentScreen()== ScreenList.SOLV )
166
        {
167
        RubikScreenSolving solving = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
168
        mNewRecord = solving.getRecord();
169

  
170
        if( mNewRecord< 0 )
171
          {
172
          mNewRecord = -mNewRecord;
173
          mIsNewRecord = false;
174
          }
175
        else
176
          {
177
          mIsNewRecord = true;
178
          }
179
        }
180

  
148
      mActioner.onSolved();
181 149
      unblockEverything();
182 150
      doEffectNow( BaseEffect.Type.WIN );
183 151
      }
......
279 247
    mScrambleObject = false;
280 248
    mIsSolved       = false;
281 249
    blockEverything(BlockController.RUBIK_PLACE_3);
282
    RubikScores.getInstance().incrementNumPlays();
283 250
    doEffectNow( BaseEffect.Type.SCRAMBLE );
284 251
    }
285 252

  
......
333 300
    mView.setQuat();
334 301
    }
335 302

  
336
///////////////////////////////////////////////////////////////////////////////////////////////////
337

  
338
  private void reportRecord()
339
    {
340
    RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
341
    RubikScores scores = RubikScores.getInstance();
342

  
343
    int object      = play.getObject();
344
    int level       = play.getLevel();
345
    ObjectType list = ObjectType.getObject(object);
346
    String name     = scores.getName();
347

  
348
    String record = list.name()+" level "+level+" time "+mNewRecord+" isNew: "+mIsNewRecord+" scrambleNum: "+mScrambleObjectNum;
349

  
350
    if( BuildConfig.DEBUG )
351
       {
352
       android.util.Log.e("pre", mDebug);
353
       android.util.Log.e("pre", name);
354
       android.util.Log.e("pre", record);
355
       }
356
    else
357
      {
358
      final RubikActivity act = (RubikActivity)mView.getContext();
359
      FirebaseAnalytics analytics = act.getAnalytics();
360

  
361
      if( analytics!=null )
362
        {
363
        Bundle bundle = new Bundle();
364
        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, mDebug);
365
        bundle.putString(FirebaseAnalytics.Param.CHARACTER, name);
366
        bundle.putString(FirebaseAnalytics.Param.LEVEL, record);
367
        analytics.logEvent(FirebaseAnalytics.Event.LEVEL_UP, bundle);
368
        }
369
      }
370
    }
371

  
372
///////////////////////////////////////////////////////////////////////////////////////////////////
373

  
374
  private void requestReview()
375
    {
376
    final RubikScores scores = RubikScores.getInstance();
377
    int numWins = scores.incrementNumWins();
378

  
379
    if( numWins==7 || numWins==30 || numWins==100 || numWins==200)
380
      {
381
      final long timeBegin = System.currentTimeMillis();
382
      final RubikActivity act = (RubikActivity)mView.getContext();
383
      final ReviewManager manager = ReviewManagerFactory.create(act);
384
      Task<ReviewInfo> request = manager.requestReviewFlow();
385

  
386
      request.addOnCompleteListener(new OnCompleteListener<ReviewInfo>()
387
        {
388
        @Override
389
        public void onComplete (@NonNull Task<ReviewInfo> task)
390
          {
391
          if (task.isSuccessful())
392
            {
393
            final String name = scores.getName();
394
            ReviewInfo reviewInfo = task.getResult();
395
            Task<Void> flow = manager.launchReviewFlow(act, reviewInfo);
396

  
397
            flow.addOnFailureListener(new OnFailureListener()
398
              {
399
              @Override
400
              public void onFailure(Exception e)
401
                {
402
                analyticsReport(act,"Failed", name, timeBegin);
403
                }
404
              });
405

  
406
            flow.addOnCompleteListener(new OnCompleteListener<Void>()
407
              {
408
              @Override
409
              public void onComplete(@NonNull Task<Void> task)
410
                {
411
                analyticsReport(act,"Complete", name, timeBegin);
412
                }
413
              });
414
            }
415
          else
416
            {
417
            String name = scores.getName();
418
            analyticsReport(act,"Not Successful", name, timeBegin);
419
            }
420
          }
421
        });
422
      }
423
    }
424

  
425
///////////////////////////////////////////////////////////////////////////////////////////////////
426

  
427
  private void analyticsReport(RubikActivity act, String message, String name, long timeBegin)
428
    {
429
    long elapsed = System.currentTimeMillis() - timeBegin;
430
    String msg = message+" startTime: "+timeBegin+" elapsed: "+elapsed+" name: "+name;
431

  
432
    if( BuildConfig.DEBUG )
433
       {
434
       android.util.Log.d("pre", msg);
435
       }
436
    else
437
      {
438
      FirebaseAnalytics analytics = act.getAnalytics();
439

  
440
      if( analytics!=null )
441
        {
442
        Bundle bundle = new Bundle();
443
        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, msg);
444
        analytics.logEvent(FirebaseAnalytics.Event.SHARE, bundle);
445
        }
446
      }
447
    }
448

  
449 303
///////////////////////////////////////////////////////////////////////////////////////////////////
450 304
//
451 305
///////////////////////////////////////////////////////////////////////////////////////////////////
......
720 574

  
721 575
          if( i==BaseEffect.Type.SCRAMBLE.ordinal() )
722 576
            {
723
            final RubikActivity act = (RubikActivity)mView.getContext();
724

  
725
            act.runOnUiThread(new Runnable()
726
              {
727
              @Override
728
              public void run()
729
                {
730
                ScreenList.switchScreen( act, ScreenList.READ);
731
                }
732
              });
577
            RubikActivity act = (RubikActivity)mView.getContext();
578
            mActioner.onScrambleEffectFinished(act);
733 579
            }
734 580

  
735 581
          if( i==BaseEffect.Type.WIN.ordinal() )
736 582
            {
737
            if( ScreenList.getCurrentScreen()== ScreenList.SOLV )
738
              {
739
              final RubikActivity act = (RubikActivity)mView.getContext();
740
              Bundle bundle = new Bundle();
741
              bundle.putLong("time", mNewRecord );
742

  
743
              reportRecord();
744
              requestReview();
745

  
746
              if( mIsNewRecord )
747
                {
748
                RubikDialogNewRecord dialog = new RubikDialogNewRecord();
749
                dialog.setArguments(bundle);
750
                dialog.show( act.getSupportFragmentManager(), RubikDialogNewRecord.getDialogTag() );
751
                }
752
              else
753
                {
754
                RubikDialogSolved dialog = new RubikDialogSolved();
755
                dialog.setArguments(bundle);
756
                dialog.show( act.getSupportFragmentManager(), RubikDialogSolved.getDialogTag() );
757
                }
758

  
759
              act.runOnUiThread(new Runnable()
760
                {
761
                @Override
762
                public void run()
763
                  {
764
                  ScreenList.switchScreen( act, ScreenList.DONE);
765
                  }
766
                });
767
              }
583
            RubikActivity act = (RubikActivity)mView.getContext();
584
            mActioner.onWinEffectFinished(act,mDebug,mScrambleObjectNum);
768 585
            }
769 586

  
770 587
          break;
src/main/java/org/distorted/main/RubikSurfaceView.java
65 65

  
66 66
    private RubikRenderer mRenderer;
67 67
    private RubikPreRender mPreRender;
68
    private RubikObjectStateActioner mActioner;
68 69
    private Movement mMovement;
69 70
    private boolean mDragging, mBeginningRotation, mContinuingRotation;
70 71
    private int mScreenWidth, mScreenHeight, mScreenMin;
......
534 535
        mFirstIndex =0;
535 536
        mLastIndex  =0;
536 537

  
538
        mActioner  = new RubikObjectStateActioner();
537 539
        mRenderer  = new RubikRenderer(this);
538
        mPreRender = new RubikPreRender(this);
540
        mPreRender = new RubikPreRender(this,mActioner);
539 541

  
540 542
        RubikActivity act = (RubikActivity)context;
541 543
        DisplayMetrics dm = new DisplayMetrics();
src/main/java/org/distorted/tutorials/TutorialObjectStateActioner.java
1
///////////////////////////////////////////////////////////////////////////////////////////////////
2
// Copyright 2019 Leszek Koltunski                                                               //
3
//                                                                                               //
4
// This file is part of Magic Cube.                                                              //
5
//                                                                                               //
6
// Magic Cube is free software: you can redistribute it and/or modify                            //
7
// it under the terms of the GNU General Public License as published by                          //
8
// the Free Software Foundation, either version 2 of the License, or                             //
9
// (at your option) any later version.                                                           //
10
//                                                                                               //
11
// Magic Cube is distributed in the hope that it will be useful,                                 //
12
// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
13
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
14
// GNU General Public License for more details.                                                  //
15
//                                                                                               //
16
// You should have received a copy of the GNU General Public License                             //
17
// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
18
///////////////////////////////////////////////////////////////////////////////////////////////////
19

  
20
package org.distorted.tutorials;
21

  
22
import org.distorted.objectlib.helpers.ObjectStateActioner;
23
import org.distorted.objectlib.helpers.TwistyActivity;
24

  
25
///////////////////////////////////////////////////////////////////////////////////////////////////
26

  
27
public class TutorialObjectStateActioner implements ObjectStateActioner
28
{
29
   public void onWinEffectFinished(TwistyActivity act, String debug, int scrambleNum) { }
30
   public void onScrambleEffectFinished(TwistyActivity act) { }
31
   public void onSolved() { }
32
}
src/main/java/org/distorted/tutorials/TutorialPreRender.java
22 22
import android.content.Context;
23 23
import android.content.res.Resources;
24 24

  
25
import org.distorted.objectlib.helpers.ObjectStateActioner;
25 26
import org.distorted.objectlib.main.ObjectType;
26 27
import org.distorted.objectlib.main.TwistyObject;
27 28
import org.distorted.objectlib.helpers.BlockController;
28 29
import org.distorted.objectlib.helpers.MovesFinished;
29 30
import org.distorted.objectlib.helpers.TwistyPreRender;
30

  
31 31
import org.distorted.objectlib.effects.BaseEffect;
32 32
import org.distorted.objectlib.effects.EffectController;
33 33

  
......
52 52
  private int mNearestAngle;
53 53
  private int mScrambleObjectNum;
54 54
  private final BlockController mBlockController;
55
  private final ObjectStateActioner mActioner;
55 56

  
56 57
///////////////////////////////////////////////////////////////////////////////////////////////////
57 58

  
58
  TutorialPreRender(TutorialSurfaceView view)
59
  TutorialPreRender(TutorialSurfaceView view, ObjectStateActioner actioner)
59 60
    {
60 61
    mView = view;
62
    mActioner = actioner;
61 63

  
62 64
    mFinishRotation = false;
63 65
    mRemoveRotation = false;
src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
49 49
    private final Static4D CAMERA_POINT = new Static4D(0, 0, 0, 0);
50 50
    private TutorialRenderer mRenderer;
51 51
    private TutorialPreRender mPreRender;
52
    private TutorialObjectStateActioner mActioner;
52 53
    private Movement mMovement;
53 54
    private boolean mDragging, mBeginningRotation, mContinuingRotation;
54 55
    private int mScreenWidth, mScreenHeight, mScreenMin;
......
569 570
        mFirstIndex =0;
570 571
        mLastIndex  =0;
571 572

  
573
        mActioner  = new TutorialObjectStateActioner();
572 574
        mRenderer  = new TutorialRenderer(this);
573
        mPreRender = new TutorialPreRender(this);
575
        mPreRender = new TutorialPreRender(this,mActioner);
574 576

  
575 577
        TutorialActivity act = (TutorialActivity)context;
576 578
        DisplayMetrics dm = new DisplayMetrics();

Also available in: Unified diff