Home | History | Annotate | Download | only in camera
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.camera;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.IntentFilter;
     23 import android.graphics.Bitmap;
     24 import android.graphics.BitmapFactory;
     25 import android.graphics.Rect;
     26 import android.hardware.Camera.Parameters;
     27 import android.net.Uri;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.Message;
     31 import android.view.KeyEvent;
     32 import android.view.View;
     33 import android.view.Window;
     34 import android.view.WindowManager;
     35 import android.view.animation.AlphaAnimation;
     36 import android.view.animation.Animation;
     37 import android.view.animation.DecelerateInterpolator;
     38 
     39 import com.android.camera.ui.LayoutChangeNotifier;
     40 import com.android.camera.ui.PopupManager;
     41 import com.android.gallery3d.app.AbstractGalleryActivity;
     42 import com.android.gallery3d.app.AppBridge;
     43 import com.android.gallery3d.app.FilmstripPage;
     44 import com.android.gallery3d.app.GalleryActionBar;
     45 import com.android.gallery3d.app.PhotoPage;
     46 import com.android.gallery3d.common.ApiHelper;
     47 import com.android.gallery3d.ui.ScreenNail;
     48 import com.android.gallery3d.util.MediaSetUtils;
     49 
     50 /**
     51  * Superclass of camera activity.
     52  */
     53 public abstract class ActivityBase extends AbstractGalleryActivity
     54         implements LayoutChangeNotifier.Listener {
     55 
     56     private static final String TAG = "ActivityBase";
     57     private static final int CAMERA_APP_VIEW_TOGGLE_TIME = 100;  // milliseconds
     58     private static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE =
     59             "android.media.action.STILL_IMAGE_CAMERA_SECURE";
     60     public static final String ACTION_IMAGE_CAPTURE_SECURE =
     61             "android.media.action.IMAGE_CAPTURE_SECURE";
     62     // The intent extra for camera from secure lock screen. True if the gallery
     63     // should only show newly captured pictures. sSecureAlbumId does not
     64     // increment. This is used when switching between camera, camcorder, and
     65     // panorama. If the extra is not set, it is in the normal camera mode.
     66     public static final String SECURE_CAMERA_EXTRA = "secure_camera";
     67 
     68     private int mResultCodeForTesting;
     69     private Intent mResultDataForTesting;
     70     private OnScreenHint mStorageHint;
     71     private View mSingleTapArea;
     72 
     73     protected boolean mOpenCameraFail;
     74     protected boolean mCameraDisabled;
     75     protected CameraManager.CameraProxy mCameraDevice;
     76     protected Parameters mParameters;
     77     // The activity is paused. The classes that extend this class should set
     78     // mPaused the first thing in onResume/onPause.
     79     protected boolean mPaused;
     80     protected GalleryActionBar mActionBar;
     81 
     82     // multiple cameras support
     83     protected int mNumberOfCameras;
     84     protected int mCameraId;
     85     // The activity is going to switch to the specified camera id. This is
     86     // needed because texture copy is done in GL thread. -1 means camera is not
     87     // switching.
     88     protected int mPendingSwitchCameraId = -1;
     89 
     90     protected MyAppBridge mAppBridge;
     91     protected ScreenNail mCameraScreenNail; // This shows camera preview.
     92     // The view containing only camera related widgets like control panel,
     93     // indicator bar, focus indicator and etc.
     94     protected View mCameraAppView;
     95     protected boolean mShowCameraAppView = true;
     96     private Animation mCameraAppViewFadeIn;
     97     private Animation mCameraAppViewFadeOut;
     98     // Secure album id. This should be incremented every time the camera is
     99     // launched from the secure lock screen. The id should be the same when
    100     // switching between camera, camcorder, and panorama.
    101     protected static int sSecureAlbumId;
    102     // True if the camera is started from secure lock screen.
    103     protected boolean mSecureCamera;
    104     private static boolean sFirstStartAfterScreenOn = true;
    105 
    106     private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD;
    107     private static final int UPDATE_STORAGE_HINT = 0;
    108     private final Handler mHandler = new Handler() {
    109             @Override
    110             public void handleMessage(Message msg) {
    111                 switch (msg.what) {
    112                     case UPDATE_STORAGE_HINT:
    113                         updateStorageHint();
    114                         return;
    115                 }
    116             }
    117     };
    118 
    119     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    120         @Override
    121         public void onReceive(Context context, Intent intent) {
    122             String action = intent.getAction();
    123             if (action.equals(Intent.ACTION_MEDIA_MOUNTED)
    124                     || action.equals(Intent.ACTION_MEDIA_UNMOUNTED)
    125                     || action.equals(Intent.ACTION_MEDIA_CHECKING)
    126                     || action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
    127                 updateStorageSpaceAndHint();
    128             }
    129         }
    130     };
    131 
    132     // close activity when screen turns off
    133     private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
    134         @Override
    135         public void onReceive(Context context, Intent intent) {
    136             finish();
    137         }
    138     };
    139 
    140     private static BroadcastReceiver sScreenOffReceiver;
    141     private static class ScreenOffReceiver extends BroadcastReceiver {
    142         @Override
    143         public void onReceive(Context context, Intent intent) {
    144             sFirstStartAfterScreenOn = true;
    145         }
    146     }
    147 
    148     public static boolean isFirstStartAfterScreenOn() {
    149         return sFirstStartAfterScreenOn;
    150     }
    151 
    152     public static void resetFirstStartAfterScreenOn() {
    153         sFirstStartAfterScreenOn = false;
    154     }
    155 
    156     protected class CameraOpenThread extends Thread {
    157         @Override
    158         public void run() {
    159             try {
    160                 mCameraDevice = Util.openCamera(ActivityBase.this, mCameraId);
    161                 mParameters = mCameraDevice.getParameters();
    162             } catch (CameraHardwareException e) {
    163                 mOpenCameraFail = true;
    164             } catch (CameraDisabledException e) {
    165                 mCameraDisabled = true;
    166             }
    167         }
    168     }
    169 
    170     @Override
    171     public void onCreate(Bundle icicle) {
    172         super.disableToggleStatusBar();
    173         // Set a theme with action bar. It is not specified in manifest because
    174         // we want to hide it by default. setTheme must happen before
    175         // setContentView.
    176         //
    177         // This must be set before we call super.onCreate(), where the window's
    178         // background is removed.
    179         setTheme(R.style.Theme_Gallery);
    180         getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
    181         if (ApiHelper.HAS_ACTION_BAR) {
    182             requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
    183         } else {
    184             requestWindowFeature(Window.FEATURE_NO_TITLE);
    185         }
    186 
    187         // Check if this is in the secure camera mode.
    188         Intent intent = getIntent();
    189         String action = intent.getAction();
    190         if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action)) {
    191             mSecureCamera = true;
    192             // Use a new album when this is started from the lock screen.
    193             sSecureAlbumId++;
    194         } else if (ACTION_IMAGE_CAPTURE_SECURE.equals(action)) {
    195             mSecureCamera = true;
    196         } else {
    197             mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false);
    198         }
    199         if (mSecureCamera) {
    200             IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
    201             registerReceiver(mScreenOffReceiver, filter);
    202             if (sScreenOffReceiver == null) {
    203                 sScreenOffReceiver = new ScreenOffReceiver();
    204                 getApplicationContext().registerReceiver(sScreenOffReceiver, filter);
    205             }
    206         }
    207         super.onCreate(icicle);
    208     }
    209 
    210     public boolean isPanoramaActivity() {
    211         return false;
    212     }
    213 
    214     @Override
    215     protected void onResume() {
    216         super.onResume();
    217 
    218         installIntentFilter();
    219         if (updateStorageHintOnResume()) {
    220             updateStorageSpace();
    221             mHandler.sendEmptyMessageDelayed(UPDATE_STORAGE_HINT, 200);
    222         }
    223     }
    224 
    225     @Override
    226     protected void onPause() {
    227         super.onPause();
    228 
    229         if (mStorageHint != null) {
    230             mStorageHint.cancel();
    231             mStorageHint = null;
    232         }
    233 
    234         unregisterReceiver(mReceiver);
    235     }
    236 
    237     @Override
    238     public void setContentView(int layoutResID) {
    239         super.setContentView(layoutResID);
    240         // getActionBar() should be after setContentView
    241         mActionBar = new GalleryActionBar(this);
    242         mActionBar.hide();
    243     }
    244 
    245     @Override
    246     public boolean onSearchRequested() {
    247         return false;
    248     }
    249 
    250     @Override
    251     public boolean onKeyDown(int keyCode, KeyEvent event) {
    252         // Prevent software keyboard or voice search from showing up.
    253         if (keyCode == KeyEvent.KEYCODE_SEARCH
    254                 || keyCode == KeyEvent.KEYCODE_MENU) {
    255             if (event.isLongPress()) return true;
    256         }
    257         if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraAppView) {
    258             return true;
    259         }
    260 
    261         return super.onKeyDown(keyCode, event);
    262     }
    263 
    264     @Override
    265     public boolean onKeyUp(int keyCode, KeyEvent event) {
    266         if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraAppView) {
    267             return true;
    268         }
    269         return super.onKeyUp(keyCode, event);
    270     }
    271 
    272     protected void setResultEx(int resultCode) {
    273         mResultCodeForTesting = resultCode;
    274         setResult(resultCode);
    275     }
    276 
    277     protected void setResultEx(int resultCode, Intent data) {
    278         mResultCodeForTesting = resultCode;
    279         mResultDataForTesting = data;
    280         setResult(resultCode, data);
    281     }
    282 
    283     public int getResultCode() {
    284         return mResultCodeForTesting;
    285     }
    286 
    287     public Intent getResultData() {
    288         return mResultDataForTesting;
    289     }
    290 
    291     @Override
    292     protected void onDestroy() {
    293         PopupManager.removeInstance(this);
    294         if (mSecureCamera) unregisterReceiver(mScreenOffReceiver);
    295         super.onDestroy();
    296     }
    297 
    298     protected void installIntentFilter() {
    299         // install an intent filter to receive SD card related events.
    300         IntentFilter intentFilter =
    301                 new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
    302         intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
    303         intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
    304         intentFilter.addAction(Intent.ACTION_MEDIA_CHECKING);
    305         intentFilter.addDataScheme("file");
    306         registerReceiver(mReceiver, intentFilter);
    307     }
    308 
    309     protected void updateStorageSpace() {
    310         mStorageSpace = Storage.getAvailableSpace();
    311     }
    312 
    313     protected long getStorageSpace() {
    314         return mStorageSpace;
    315     }
    316 
    317     protected void updateStorageSpaceAndHint() {
    318         updateStorageSpace();
    319         updateStorageHint(mStorageSpace);
    320     }
    321 
    322     protected void updateStorageHint() {
    323         updateStorageHint(mStorageSpace);
    324     }
    325 
    326     protected boolean updateStorageHintOnResume() {
    327         return true;
    328     }
    329 
    330     protected void updateStorageHint(long storageSpace) {
    331         String message = null;
    332         if (storageSpace == Storage.UNAVAILABLE) {
    333             message = getString(R.string.no_storage);
    334         } else if (storageSpace == Storage.PREPARING) {
    335             message = getString(R.string.preparing_sd);
    336         } else if (storageSpace == Storage.UNKNOWN_SIZE) {
    337             message = getString(R.string.access_sd_fail);
    338         } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD) {
    339             message = getString(R.string.spaceIsLow_content);
    340         }
    341 
    342         if (message != null) {
    343             if (mStorageHint == null) {
    344                 mStorageHint = OnScreenHint.makeText(this, message);
    345             } else {
    346                 mStorageHint.setText(message);
    347             }
    348             mStorageHint.show();
    349         } else if (mStorageHint != null) {
    350             mStorageHint.cancel();
    351             mStorageHint = null;
    352         }
    353     }
    354 
    355     protected void gotoGallery() {
    356         // Move the next picture with capture animation. "1" means next.
    357         mAppBridge.switchWithCaptureAnimation(1);
    358     }
    359 
    360     // Call this after setContentView.
    361     public ScreenNail createCameraScreenNail(boolean getPictures) {
    362         mCameraAppView = findViewById(R.id.camera_app_root);
    363         Bundle data = new Bundle();
    364         String path;
    365         if (getPictures) {
    366             if (mSecureCamera) {
    367                 path = "/secure/all/" + sSecureAlbumId;
    368             } else {
    369                 path = "/local/all/" + MediaSetUtils.CAMERA_BUCKET_ID;
    370             }
    371         } else {
    372             path = "/local/all/0"; // Use 0 so gallery does not show anything.
    373         }
    374         data.putString(PhotoPage.KEY_MEDIA_SET_PATH, path);
    375         data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path);
    376         data.putBoolean(PhotoPage.KEY_SHOW_WHEN_LOCKED, mSecureCamera);
    377 
    378         // Send an AppBridge to gallery to enable the camera preview.
    379         if (mAppBridge != null) {
    380             mCameraScreenNail.recycle();
    381         }
    382         mAppBridge = new MyAppBridge();
    383         data.putParcelable(PhotoPage.KEY_APP_BRIDGE, mAppBridge);
    384         if (getStateManager().getStateCount() == 0) {
    385             getStateManager().startState(FilmstripPage.class, data);
    386         } else {
    387             getStateManager().switchState(getStateManager().getTopState(),
    388                     FilmstripPage.class, data);
    389         }
    390         mCameraScreenNail = mAppBridge.getCameraScreenNail();
    391         return mCameraScreenNail;
    392     }
    393 
    394     // Call this after setContentView.
    395     protected ScreenNail reuseCameraScreenNail(boolean getPictures) {
    396         mCameraAppView = findViewById(R.id.camera_app_root);
    397         Bundle data = new Bundle();
    398         String path;
    399         if (getPictures) {
    400             if (mSecureCamera) {
    401                 path = "/secure/all/" + sSecureAlbumId;
    402             } else {
    403                 path = "/local/all/" + MediaSetUtils.CAMERA_BUCKET_ID;
    404             }
    405         } else {
    406             path = "/local/all/0"; // Use 0 so gallery does not show anything.
    407         }
    408         data.putString(PhotoPage.KEY_MEDIA_SET_PATH, path);
    409         data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path);
    410         data.putBoolean(PhotoPage.KEY_SHOW_WHEN_LOCKED, mSecureCamera);
    411 
    412         // Send an AppBridge to gallery to enable the camera preview.
    413         if (mAppBridge == null) {
    414             mAppBridge = new MyAppBridge();
    415         }
    416         data.putParcelable(PhotoPage.KEY_APP_BRIDGE, mAppBridge);
    417         if (getStateManager().getStateCount() == 0) {
    418             getStateManager().startState(FilmstripPage.class, data);
    419         }
    420         mCameraScreenNail = mAppBridge.getCameraScreenNail();
    421         return mCameraScreenNail;
    422     }
    423 
    424     private class HideCameraAppView implements Animation.AnimationListener {
    425         @Override
    426         public void onAnimationEnd(Animation animation) {
    427             // We cannot set this as GONE because we want to receive the
    428             // onLayoutChange() callback even when we are invisible.
    429             mCameraAppView.setVisibility(View.INVISIBLE);
    430         }
    431 
    432         @Override
    433         public void onAnimationRepeat(Animation animation) {
    434         }
    435 
    436         @Override
    437         public void onAnimationStart(Animation animation) {
    438         }
    439     }
    440 
    441     protected void updateCameraAppView() {
    442         // Initialize the animation.
    443         if (mCameraAppViewFadeIn == null) {
    444             mCameraAppViewFadeIn = new AlphaAnimation(0f, 1f);
    445             mCameraAppViewFadeIn.setDuration(CAMERA_APP_VIEW_TOGGLE_TIME);
    446             mCameraAppViewFadeIn.setInterpolator(new DecelerateInterpolator());
    447 
    448             mCameraAppViewFadeOut = new AlphaAnimation(1f, 0f);
    449             mCameraAppViewFadeOut.setDuration(CAMERA_APP_VIEW_TOGGLE_TIME);
    450             mCameraAppViewFadeOut.setInterpolator(new DecelerateInterpolator());
    451             mCameraAppViewFadeOut.setAnimationListener(new HideCameraAppView());
    452         }
    453 
    454         if (mShowCameraAppView) {
    455             mCameraAppView.setVisibility(View.VISIBLE);
    456             // The "transparent region" is not recomputed when a sibling of
    457             // SurfaceView changes visibility (unless it involves GONE). It's
    458             // been broken since 1.0. Call requestLayout to work around it.
    459             mCameraAppView.requestLayout();
    460             mCameraAppView.startAnimation(mCameraAppViewFadeIn);
    461         } else {
    462             mCameraAppView.startAnimation(mCameraAppViewFadeOut);
    463         }
    464     }
    465 
    466     protected void onFullScreenChanged(boolean full) {
    467         if (mShowCameraAppView == full) return;
    468         mShowCameraAppView = full;
    469         if (mPaused || isFinishing()) return;
    470         updateCameraAppView();
    471     }
    472 
    473     @Override
    474     public GalleryActionBar getGalleryActionBar() {
    475         return mActionBar;
    476     }
    477 
    478     // Preview frame layout has changed.
    479     @Override
    480     public void onLayoutChange(View v, int left, int top, int right, int bottom) {
    481         if (mAppBridge == null) return;
    482 
    483         int width = right - left;
    484         int height = bottom - top;
    485         if (ApiHelper.HAS_SURFACE_TEXTURE) {
    486             CameraScreenNail screenNail = (CameraScreenNail) mCameraScreenNail;
    487             if (Util.getDisplayRotation(this) % 180 == 0) {
    488                 screenNail.setPreviewFrameLayoutSize(width, height);
    489             } else {
    490                 // Swap the width and height. Camera screen nail draw() is based on
    491                 // natural orientation, not the view system orientation.
    492                 screenNail.setPreviewFrameLayoutSize(height, width);
    493             }
    494             notifyScreenNailChanged();
    495         }
    496     }
    497 
    498     protected void setSingleTapUpListener(View singleTapArea) {
    499         mSingleTapArea = singleTapArea;
    500     }
    501 
    502     private boolean onSingleTapUp(int x, int y) {
    503         // Ignore if listener is null or the camera control is invisible.
    504         if (mSingleTapArea == null || !mShowCameraAppView) return false;
    505 
    506         int[] relativeLocation = Util.getRelativeLocation((View) getGLRoot(),
    507                 mSingleTapArea);
    508         x -= relativeLocation[0];
    509         y -= relativeLocation[1];
    510         if (x >= 0 && x < mSingleTapArea.getWidth() && y >= 0
    511                 && y < mSingleTapArea.getHeight()) {
    512             onSingleTapUp(mSingleTapArea, x, y);
    513             return true;
    514         }
    515         return false;
    516     }
    517 
    518     protected void onSingleTapUp(View view, int x, int y) {
    519     }
    520 
    521     public void setSwipingEnabled(boolean enabled) {
    522         mAppBridge.setSwipingEnabled(enabled);
    523     }
    524 
    525     public void notifyScreenNailChanged() {
    526         mAppBridge.notifyScreenNailChanged();
    527     }
    528 
    529     protected void onPreviewTextureCopied() {
    530     }
    531 
    532     protected void onCaptureTextureCopied() {
    533     }
    534 
    535     protected void addSecureAlbumItemIfNeeded(boolean isVideo, Uri uri) {
    536         if (mSecureCamera) {
    537             int id = Integer.parseInt(uri.getLastPathSegment());
    538             mAppBridge.addSecureAlbumItem(isVideo, id);
    539         }
    540     }
    541 
    542     public boolean isSecureCamera() {
    543         return mSecureCamera;
    544     }
    545 
    546     //////////////////////////////////////////////////////////////////////////
    547     //  The is the communication interface between the Camera Application and
    548     //  the Gallery PhotoPage.
    549     //////////////////////////////////////////////////////////////////////////
    550 
    551     class MyAppBridge extends AppBridge implements CameraScreenNail.Listener {
    552         @SuppressWarnings("hiding")
    553         private ScreenNail mCameraScreenNail;
    554         private Server mServer;
    555 
    556         @Override
    557         public ScreenNail attachScreenNail() {
    558             if (mCameraScreenNail == null) {
    559                 if (ApiHelper.HAS_SURFACE_TEXTURE) {
    560                     mCameraScreenNail = new CameraScreenNail(this);
    561                 } else {
    562                     Bitmap b = BitmapFactory.decodeResource(getResources(),
    563                             R.drawable.wallpaper_picker_preview);
    564                     mCameraScreenNail = new StaticBitmapScreenNail(b);
    565                 }
    566             }
    567             return mCameraScreenNail;
    568         }
    569 
    570         @Override
    571         public void detachScreenNail() {
    572             mCameraScreenNail = null;
    573         }
    574 
    575         public ScreenNail getCameraScreenNail() {
    576             return mCameraScreenNail;
    577         }
    578 
    579         // Return true if the tap is consumed.
    580         @Override
    581         public boolean onSingleTapUp(int x, int y) {
    582             return ActivityBase.this.onSingleTapUp(x, y);
    583         }
    584 
    585         // This is used to notify that the screen nail will be drawn in full screen
    586         // or not in next draw() call.
    587         @Override
    588         public void onFullScreenChanged(boolean full) {
    589             ActivityBase.this.onFullScreenChanged(full);
    590         }
    591 
    592         @Override
    593         public void requestRender() {
    594             getGLRoot().requestRenderForced();
    595         }
    596 
    597         @Override
    598         public void onPreviewTextureCopied() {
    599             ActivityBase.this.onPreviewTextureCopied();
    600         }
    601 
    602         @Override
    603         public void onCaptureTextureCopied() {
    604             ActivityBase.this.onCaptureTextureCopied();
    605         }
    606 
    607         @Override
    608         public void setServer(Server s) {
    609             mServer = s;
    610         }
    611 
    612         @Override
    613         public boolean isPanorama() {
    614             return ActivityBase.this.isPanoramaActivity();
    615         }
    616 
    617         @Override
    618         public boolean isStaticCamera() {
    619             return !ApiHelper.HAS_SURFACE_TEXTURE;
    620         }
    621 
    622         public void addSecureAlbumItem(boolean isVideo, int id) {
    623             if (mServer != null) mServer.addSecureAlbumItem(isVideo, id);
    624         }
    625 
    626         private void setCameraRelativeFrame(Rect frame) {
    627             if (mServer != null) mServer.setCameraRelativeFrame(frame);
    628         }
    629 
    630         private void switchWithCaptureAnimation(int offset) {
    631             if (mServer != null) mServer.switchWithCaptureAnimation(offset);
    632         }
    633 
    634         private void setSwipingEnabled(boolean enabled) {
    635             if (mServer != null) mServer.setSwipingEnabled(enabled);
    636         }
    637 
    638         private void notifyScreenNailChanged() {
    639             if (mServer != null) mServer.notifyScreenNailChanged();
    640         }
    641     }
    642 }
    643