191acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang/*
291acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * Copyright (C) 2009 The Android Open Source Project
391acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang *
491acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * Licensed under the Apache License, Version 2.0 (the "License");
591acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * you may not use this file except in compliance with the License.
691acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * You may obtain a copy of the License at
791acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang *
891acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang *      http://www.apache.org/licenses/LICENSE-2.0
991acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang *
1091acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * Unless required by applicable law or agreed to in writing, software
1191acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * distributed under the License is distributed on an "AS IS" BASIS,
1291acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1391acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * See the License for the specific language governing permissions and
1491acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang * limitations under the License.
1591acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang */
1691acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang
17cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changpackage com.android.camera;
18cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
193f3c857e3f34650c15d764810335024654b0fcc3Owen Linimport static com.android.camera.Util.Assert;
203f3c857e3f34650c15d764810335024654b0fcc3Owen Lin
217add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Liimport android.hardware.Camera.CameraInfo;
224c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Changimport android.hardware.Camera.Parameters;
23f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Linimport android.os.Build;
24cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changimport android.os.Handler;
25cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changimport android.os.HandlerThread;
26cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changimport android.os.Looper;
27cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changimport android.os.Message;
28cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changimport android.util.Log;
29cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
30cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changimport java.io.IOException;
31cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
32271b3095b9f763421c0547109da9de774795072dChih-Chung Chang/**
33271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * The class is used to hold an {@code android.hardware.Camera} instance.
34271b3095b9f763421c0547109da9de774795072dChih-Chung Chang *
35271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * <p>The {@code open()} and {@code release()} calls are similar to the ones
36271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * in {@code android.hardware.Camera}. The difference is if {@code keep()} is
37271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * called before {@code release()}, CameraHolder will try to hold the {@code
38271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * android.hardware.Camera} instance for a while, so if {@code open()} is
39271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * called soon after, we can avoid the cost of {@code open()} in {@code
40271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * android.hardware.Camera}.
41271b3095b9f763421c0547109da9de774795072dChih-Chung Chang *
42271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * <p>This is used in switching between {@code Camera} and {@code VideoCamera}
43271b3095b9f763421c0547109da9de774795072dChih-Chung Chang * activities.
44271b3095b9f763421c0547109da9de774795072dChih-Chung Chang */
45cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Changpublic class CameraHolder {
46cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    private static final String TAG = "CameraHolder";
47cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    private android.hardware.Camera mCameraDevice;
4891acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang    private long mKeepBeforeTime = 0;  // Keep the Camera before this time.
493f3c857e3f34650c15d764810335024654b0fcc3Owen Lin    private final Handler mHandler;
5091acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang    private int mUsers = 0;  // number of open() - number of release()
51ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang    private int mNumberOfCameras;
52003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li    private int mCameraId = -1;  // current camera id
53003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li    private int mBackCameraId = -1, mFrontCameraId = -1;
547add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li    private CameraInfo[] mInfo;
55cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
564c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang    // We store the camera parameters when we actually open the device,
574c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang    // so we can restore them in the subsequent open() requests by the user.
584c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang    // This prevents the parameters set by the Camera activity used by
594c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang    // the VideoCamera activity inadvertently.
604c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang    private Parameters mParameters;
614c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang
62cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    // Use a singleton.
63cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    private static CameraHolder sHolder;
64cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    public static synchronized CameraHolder instance() {
65cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        if (sHolder == null) {
66cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            sHolder = new CameraHolder();
67cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        }
68cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        return sHolder;
69cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    }
70cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
71cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    private static final int RELEASE_CAMERA = 1;
72cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    private class MyHandler extends Handler {
73cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        MyHandler(Looper looper) {
74cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            super(looper);
75cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        }
76cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
77cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        @Override
78cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        public void handleMessage(Message msg) {
79cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            switch(msg.what) {
80cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang                case RELEASE_CAMERA:
81dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                    synchronized (CameraHolder.this) {
82dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                        // In 'CameraHolder.open', the 'RELEASE_CAMERA' message
83dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                        // will be removed if it is found in the queue. However,
84dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                        // there is a chance that this message has been handled
85dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                        // before being removed. So, we need to add a check
86dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                        // here:
87dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                        if (CameraHolder.this.mUsers == 0) releaseCamera();
88dd6600e7f0adf322e5a8fcb0ed5389b14655106eOwen Lin                    }
89cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang                    break;
90cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            }
91cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        }
92cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    }
93cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
94cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    private CameraHolder() {
95cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        HandlerThread ht = new HandlerThread("CameraHolder");
96cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        ht.start();
97cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        mHandler = new MyHandler(ht.getLooper());
98ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang        mNumberOfCameras = android.hardware.Camera.getNumberOfCameras();
997add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li        mInfo = new CameraInfo[mNumberOfCameras];
100ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang        for (int i = 0; i < mNumberOfCameras; i++) {
1017add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li            mInfo[i] = new CameraInfo();
1027add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li            android.hardware.Camera.getCameraInfo(i, mInfo[i]);
103003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li            if (mBackCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_BACK) {
104003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li                mBackCameraId = i;
105003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li            }
106003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li            if (mFrontCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_FRONT) {
107003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li                mFrontCameraId = i;
108003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li            }
109ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang        }
110ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang    }
111ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang
112ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang    public int getNumberOfCameras() {
113ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang        return mNumberOfCameras;
114cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    }
115cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
1167add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li    public CameraInfo[] getCameraInfo() {
1177add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li        return mInfo;
1187add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li    }
1197add00693c1ec910bc8700fe046ee18cbe4e1148Wu-cheng Li
120ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang    public synchronized android.hardware.Camera open(int cameraId)
1213f3c857e3f34650c15d764810335024654b0fcc3Owen Lin            throws CameraHardwareException {
12291acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        Assert(mUsers == 0);
12343b6525b4aedc5e177163cab7b6f26698a19097fWu-cheng Li        if (mCameraDevice != null && mCameraId != cameraId) {
12443b6525b4aedc5e177163cab7b6f26698a19097fWu-cheng Li            mCameraDevice.release();
12543b6525b4aedc5e177163cab7b6f26698a19097fWu-cheng Li            mCameraDevice = null;
12643b6525b4aedc5e177163cab7b6f26698a19097fWu-cheng Li            mCameraId = -1;
12743b6525b4aedc5e177163cab7b6f26698a19097fWu-cheng Li        }
128cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        if (mCameraDevice == null) {
1293f3c857e3f34650c15d764810335024654b0fcc3Owen Lin            try {
130ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang                Log.v(TAG, "open camera " + cameraId);
131ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang                mCameraDevice = android.hardware.Camera.open(cameraId);
13243b6525b4aedc5e177163cab7b6f26698a19097fWu-cheng Li                mCameraId = cameraId;
1333f3c857e3f34650c15d764810335024654b0fcc3Owen Lin            } catch (RuntimeException e) {
1343f3c857e3f34650c15d764810335024654b0fcc3Owen Lin                Log.e(TAG, "fail to connect Camera", e);
135f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Lin                throw new CameraHardwareException(e);
1363f3c857e3f34650c15d764810335024654b0fcc3Owen Lin            }
1374c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang            mParameters = mCameraDevice.getParameters();
138cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        } else {
139cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            try {
140cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang                mCameraDevice.reconnect();
141cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            } catch (IOException e) {
142cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang                Log.e(TAG, "reconnect failed.");
143f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Lin                throw new CameraHardwareException(e);
144cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            }
1454c9266ef8f43c6b057b6f560645475272c66ff8aChih-Chung Chang            mCameraDevice.setParameters(mParameters);
146cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        }
14791acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        ++mUsers;
148cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        mHandler.removeMessages(RELEASE_CAMERA);
14991acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        mKeepBeforeTime = 0;
150cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        return mCameraDevice;
151cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    }
152cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
153d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin    /**
154d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin     * Tries to open the hardware camera. If the camera is being used or
155d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin     * unavailable then return {@code null}.
156d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin     */
157ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang    public synchronized android.hardware.Camera tryOpen(int cameraId) {
158d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin        try {
159ac9d0a1ce538eb4bd50cba3b257737a05b9ac4e5Chih-Chung Chang            return mUsers == 0 ? open(cameraId) : null;
160d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin        } catch (CameraHardwareException e) {
161f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Lin            // In eng build, we throw the exception so that test tool
162f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Lin            // can detect it and report it
163f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Lin            if ("eng".equals(Build.TYPE)) {
164f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Lin                throw new RuntimeException(e);
165f6ef7b960f610d557f0bab924b9f7928b46f6f0aOwen Lin            }
166d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin            return null;
167d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin        }
168d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin    }
169d9e32402bbc5ebaac40ccd2c4b734f8e5743343eOwen Lin
170cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    public synchronized void release() {
17191acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        Assert(mUsers == 1);
17291acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        --mUsers;
173cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        mCameraDevice.stopPreview();
174cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        releaseCamera();
175cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    }
176cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
177cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    private synchronized void releaseCamera() {
17891acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        Assert(mUsers == 0);
179cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        Assert(mCameraDevice != null);
180cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        long now = System.currentTimeMillis();
18191acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        if (now < mKeepBeforeTime) {
182cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            mHandler.sendEmptyMessageDelayed(RELEASE_CAMERA,
18391acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang                    mKeepBeforeTime - now);
184cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang            return;
185cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        }
186cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        mCameraDevice.release();
187cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        mCameraDevice = null;
188788aad96ea4abcda2469867464139daa8087af65Wu-cheng Li        // We must set this to null because it has a reference to Camera.
189788aad96ea4abcda2469867464139daa8087af65Wu-cheng Li        // Camera has references to the listeners.
190788aad96ea4abcda2469867464139daa8087af65Wu-cheng Li        mParameters = null;
19143b6525b4aedc5e177163cab7b6f26698a19097fWu-cheng Li        mCameraId = -1;
192cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    }
193cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang
194cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    public synchronized void keep() {
19591acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        // We allow (mUsers == 0) for the convenience of the calling activity.
1965075f53df86f3dcb0a46fd6353057260ad480f43Chih-Chung Chang        // The activity may not have a chance to call open() before the user
1975075f53df86f3dcb0a46fd6353057260ad480f43Chih-Chung Chang        // choose the menu item to switch to another activity.
19891acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        Assert(mUsers == 1 || mUsers == 0);
199cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang        // Keep the camera instance for 3 seconds.
20091acfc99279d5ece7ac9cb2d7a2980eb0d3b50daChih-Chung Chang        mKeepBeforeTime = System.currentTimeMillis() + 3000;
201cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang    }
202003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li
203003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li    public int getBackCameraId() {
204003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li        return mBackCameraId;
205003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li    }
206003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li
207003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li    public int getFrontCameraId() {
208003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li        return mFrontCameraId;
209003dd5a52457c024a0f99a2bb222bfc6ad70bbe5Wu-cheng Li    }
210cd65be31531717fb032b7423f8d5a77df465cfcaChih-Chung Chang}
211