Pong

Tap or drag on the screen to move the paddle. The ball will move by itself, bouncing off the paddle and the edges of the screen. The app launches and remains in landscape orientation. On Mac, type fn-control-F12 to rotate the Android emulator to landscape orientation.

The infinite loop (the for (;;) {) repositions the ball 60 time a second. It is our first piece of code that is executed by a thread other than the main thread (a.k.a. the UI thread).

Thus far we have been doing everything in the UI thread, which has always been our only thread. But the infinite loop would prevent the UI thread from doing its other jobs, such as detecting touches. (For starters, an infinite loop in the View’s constructor would prevent us from getting to the end of the constructor, and would therefore prevent the other lifecycle methods from executing.) We therefore execute the infinite loop in a separate thread. Code in the UI thread is allowed to call the invalidate method of a View. Code in another thread must call the postInvalidate method of the View. See Communicating with the UI Thread.

Having more than one thread increases the number of places where we can write code. Until now, we have been able to write code in only one place: in the lifecycle methods of the Activity object (onCreate, onStart, etc.) and in the methods called by them. Now we can have code than runs as long as we want—even forever—in the parallel universe of another thread.

Source code in Pong.zip

  1. MainActivity.java: onCreate creates a PongView.
  2. PongView.java
  3. AndroidManifest.xml. The <activity> element has the attribute android:screenOrientation="landscape".
  4. build.gradle (Module: app).

List the thread

See Threads. In the PongView constructor, change

        Thread thread = new Thread(runnable); //a thread that is not the UI thread
        thread.start(); //The thread will execute the run method of the Runnable object.
to
        Thread thread = new Thread(runnable);
        Log.d("myTag", "id of thread = " + thread.getId());
        Log.d("myTag", "name of thread = " + thread.getName());
        Log.d("myTag", "state of thread = " + thread.getState());

        thread.start();
        Log.d("myTag", "state of thread = " + thread.getState());

id of thread = 81
name of thread = Thread-81
state of thread = NEW
state of thread = WAITING

The ps command (“process status”) lists all the processes and threads that exist.

adb devices
List of devices attached
ca1784a34445a8d0308	device
192.168.57.101:5555	device
emulator-5554	device

adb -s 192.168.57.101:5555 shell ps
USER     PID   PPID  VSIZE  RSS     WCHAN    PC         NAME
root      1     0     720    440   c021a64f 0805d2c6 S /init
root      272   1     552120 57524 ffffffff b7653770 S zygote
u0_a67    1354  272   595168 46708 ffffffff b76549f3 S edu.nyu.scps.pong
root      87    1     28424  456   ffffffff 0806c3a0 S /sbin/adbd
root      16841 87    1868   504   00000000 b76a1146 R ps

adb -s 192.168.57.101:5555 shell ps -t 1354
USER     PID   PPID  VSIZE  RSS     WCHAN    PC         NAME
u0_a67    1354  272   595108 45460 ffffffff b76549f3 S edu.nyu.scps.pong
u0_a67    1358  1354  595108 45460 c0183a45 b7655399 S GC
u0_a67    1359  1354  595108 45460 c014bfe7 b76547cb S Signal Catcher
u0_a67    1360  1354  595108 45460 c06c29bd b7654b13 S JDWP
u0_a67    1361  1354  595108 45460 c0183a45 b7655399 S Compiler
u0_a67    1362  1354  595108 45460 c0183a45 b7655399 S ReferenceQueueD
u0_a67    1363  1354  595108 45460 c0183a45 b7655399 S FinalizerDaemon
u0_a67    1364  1354  595108 45460 c0183a45 b7655399 S FinalizerWatchd
u0_a67    1365  1354  595108 45460 c05963ba b7653586 S Binder_1
u0_a67    1366  1354  595108 45460 c05963ba b7653586 S Binder_2
u0_a67    1369  1354  595108 45460 c0183a45 b7655399 S Thread-81

adb -s 192.168.57.101:5555 shell
root@vbox86p:/ # ps -t 1354
root@vbox86p:/ # exit

Another way to see the threads:
Tools → Android → Android Device Monitor
In the Devices tab in the left panel of Android Device Monitor, select your app edu.nyu.scps.pong. In the toolbar below the tab, click on the Update Threads icon. It has three parallel arrows pointing to the right. In the right panel, select the Threads tab. If some of the panels seems to be missing, click on the Maximize icon in the upper right.

If some of the monitor windows seem to be missing, pull down
Window → Reset Perspective…
Do you want to reset the current DDMS perspective to its defaults?
Yes

cd ~/.android
pwd
ls -ld monitor-workspace

Which thread is executing each method?

Insert the following code at the start of the PongView’s constructor (immediately after the super(context);) and at the start of the run method (immediately before the infinite for loop).

        Thread currentThread = Thread.currentThread();
        Log.d("myTag", "id of current thread = " + currentThread.getId());
        Log.d("myTag", "name of current thread = " + currentThread.getName());
id of current thread = 1
name of current thread = main
id of current thread = 81
name of current thread = Thread-81

Things to try

  1. Make the ball move faster or slower. Increase or decrease the dx and dy fields of the PongView, or decrease or increase the number of milliseconds of sleep time.

  2. Instead of hardcoding in the 60 hertz, use the hardware refresh rate of your display. It was approximately 68 on my Motorola MB612, 260 on the Android Emulator on my Windows Vista PC, 60 on the Samsung Galaxy S5 in Genymotion on my Mac, and 60 on my Azpen A727 tablet. Insert the following code at the start of the run method.
    		Context context = getContext();
    		WindowManager manager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    		Display display = manager.getDefaultDisplay();
    		float hertz = display.getRefreshRate();
                    Log.d("myTag", "hertz = " + hertz);
                    long milliseconds = (long)(1000f / hertz);
    
    and change the parameter of sleep to milliseconds.

  3. Fix the bug in the logic. A collision is not detected when the ball hits a corner of the paddle.

  4. If the thread calls invalidate instead of postInvalidate, Android says “Unfortunately, Pong has stopped.” What error message do you get in the logcat window of Android Studio?
    07-08 14:42:34.985  31167-31180/? E/AndroidRuntime: FATAL EXCEPTION: Thread-144
        Process: edu.nyu.scps.pong, PID: 1354
        android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    

  5. Play a beep each time the ball hits something. Create a ToneGenerator at the start of the run method.
                    ToneGenerator toneGenerator = new ToneGenerator(AudioManager.STREAM_ALARM, ToneGenerator.MAX_VOLUME / 2);
    
    After each statement that reverses the sign of dx or dy,
                            //proprietary beep, duration in milliseconds
                            toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP, 100);
    

  6. To turn off the beeping when the app is covered, remove the code we added in the previous exercise. Add the following fields and methods to class MainActivity.
        private boolean isResumed = false; //true between onResume and onPause
        private ToneGenerator toneGenerator = new ToneGenerator(AudioManager.STREAM_ALARM, ToneGenerator.MAX_VOLUME / 2);
    
        @Override
        protected void onResume() {
            super.onResume();
            isResumed = true;
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            isResumed = false;
        }
    
        public void beep() {
            if (isResumed) {
                toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP, 100);
            }
        }
    
    After each statement that reverses the sign of dx or dy,
        ((MainActivity)getContext()).beep();
    

  7. Add a second paddle for a second player.
    1. Add a new field to class PongView.
          private RectF paddle2 = new RectF();	//the paddle at the right end of the PongView
      
              //in onLayout method of class PongView
              paddle2.set(getWidth() - 3 * w, 5 * w, getWidth() - 2 * w, 10 * w);
      
              //in onDraw method of class PongView
              canvas.drawRect(paddle2, paint);
      
    2. Change onTouch to the following. Both players can touch simultaneously; see Track Multiple Pointers. (For some reason, Android calls fingers “pointers”.)
                  @Override
                  public boolean onTouch(View v, MotionEvent motionEvent) {
                      int n = motionEvent.getPointerCount();  //number of fingers touching right now
                      for (int i = 0; i < n; ++i) {
                          if (motionEvent.getX(i) < getWidth() / 2) {
                              //Finger number i touched left side of PongView.
                              paddle.offsetTo(paddle.left, motionEvent.getY(i) - paddle.height() / 2);
                          } else {
                              //Finger number i touched right side of PongView.
                              paddle2.offsetTo(paddle2.left, motionEvent.getY(i) - paddle2.height() / 2);
                          }
                      }
      
                      invalidate();
                      return true;
                  }
      
    3. Add the following code to the run method after the code for the existing paddle.
                          //If the ball is not currently embedded in paddle2,
                          if (!RectF.intersects(ball, paddle2)) {
      
                              //but will be embedded on its next move,
                              if (RectF.intersects(horizontal, paddle2)) {
                                  dx = -dx;
                              }
      
                              if (RectF.intersects(vertical, paddle2)) {
                                  dy = -dy;
                              }
                          }
      
      Instead of having two separate RectFs, paddle and paddle2, it would be more beautiful to have an array of two RectFs.