177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan/* 277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * Copyright (C) 2017 The Android Open Source Project 377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * 477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * Licensed under the Apache License, Version 2.0 (the "License"); 577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * you may not use this file except in compliance with the License. 677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * You may obtain a copy of the License at 777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * 877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * http://www.apache.org/licenses/LICENSE-2.0 977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * 1077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * Unless required by applicable law or agreed to in writing, software 1177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * distributed under the License is distributed on an "AS IS" BASIS, 1277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * See the License for the specific language governing permissions and 1477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * limitations under the License 1577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan */ 1677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanpackage com.android.car.trust; 1777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 1877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.bluetooth.BluetoothDevice; 1977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.bluetooth.BluetoothGatt; 2077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.bluetooth.BluetoothGattCharacteristic; 2177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.bluetooth.BluetoothGattService; 2277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.content.Context; 2377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.content.SharedPreferences; 2477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.os.Handler; 2577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.os.ParcelUuid; 2677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.preference.PreferenceManager; 2777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.util.Base64; 2877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.util.Log; 2977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.view.View; 3077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.widget.Button; 3177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport android.widget.TextView; 3277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport com.android.car.trust.comms.SimpleBleClient; 3377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 3477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport java.nio.ByteBuffer; 3577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport java.util.Random; 3677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanimport java.util.UUID; 3777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 3877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan/** 39452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer * A controller that sets up a {@link SimpleBleClient} to connect to the BLE enrollment service. 40452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer * It also binds the UI components to control the enrollment process. 4177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan */ 4277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chanpublic class PhoneEnrolmentController { 4377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private static final String TAG = "PhoneEnrolmentCtlr"; 4477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private String mTokenHandleKey; 4577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private String mEscrowTokenKey; 4677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 47452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer // BLE characteristics associated with the enrollment/add escrow token service. 4877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private BluetoothGattCharacteristic mEnrolmentTokenHandle; 4977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private BluetoothGattCharacteristic mEnrolmentEscrowToken; 5077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 5177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private ParcelUuid mEnrolmentServiceUuid; 5277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 5377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private SimpleBleClient mClient; 5477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private Context mContext; 5577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 5677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private TextView mTextView; 5777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private Handler mHandler; 5877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 5977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private Button mScanButton; 6077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private Button mEnrolButton; 6177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 6277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public PhoneEnrolmentController(Context context) { 6377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mContext = context; 6477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 6577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mTokenHandleKey = context.getString(R.string.pref_key_token_handle); 6677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEscrowTokenKey = context.getString(R.string.pref_key_escrow_token); 6777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 6877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mClient = new SimpleBleClient(context); 6977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolmentServiceUuid = new ParcelUuid( 70452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer UUID.fromString(mContext.getString(R.string.enrollment_service_uuid))); 7177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mClient.addCallback(mCallback /* callback */); 7277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 7377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mHandler = new Handler(mContext.getMainLooper()); 7477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 7577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 7677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan /** 7777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * Binds the views to the actions that can be performed by this controller. 7877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * 7977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * @param textView A text view used to display results from various BLE actions 8077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * @param scanButton Button used to start scanning for available BLE devices. 8177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * @param enrolButton Button used to send new escrow token to remote device. 8277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan */ 8377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void bind(TextView textView, Button scanButton, Button enrolButton) { 8477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mTextView = textView; 8577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mScanButton = scanButton; 8677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolButton = enrolButton; 8777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 8877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mScanButton.setOnClickListener(new View.OnClickListener() { 8977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 9077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void onClick(View v) { 9177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mClient.start(mEnrolmentServiceUuid); 9277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 9377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan }); 9477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 9577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolButton.setEnabled(false); 9677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolButton.setAlpha(0.3f); 9777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolButton.setOnClickListener(new View.OnClickListener() { 9877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 9977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void onClick(View v) { 10077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan appendOutputText("Sending new escrow token to remote device"); 10177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 10277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan byte[] token = generateEscrowToken(); 10377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan sendEnrolmentRequest(token); 10477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 10577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan // WARNING: Store the token so it can be used later for unlocking. This token 10677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan // should NEVER be stored on the device that is being unlocked. It should 10777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan // always be securely stored on a remote device that will trigger the unlock. 10877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan storeToken(token); 10977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 11077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan }); 11177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 11277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 11377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan /** 11477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan * @return A random byte array that is used as the escrow token for remote device unlock. 11577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan */ 11677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private byte[] generateEscrowToken() { 11777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan Random random = new Random(); 11877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); 11977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan buffer.putLong(0, random.nextLong()); 12077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan return buffer.array(); 12177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 12277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 12377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private void sendEnrolmentRequest(byte[] token) { 12477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolmentEscrowToken.setValue(token); 12577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mClient.writeCharacteristic(mEnrolmentEscrowToken); 12677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan storeToken(token); 12777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 12877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 12977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private SimpleBleClient.ClientCallback mCallback = new SimpleBleClient.ClientCallback() { 13077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 13177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void onDeviceConnected(BluetoothDevice device) { 13277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan appendOutputText("Device connected: " + device.getName() 13377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan + " addr: " + device.getAddress()); 13477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 13577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 13677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 13777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void onDeviceDisconnected() { 13877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan appendOutputText("Device disconnected"); 13977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 14077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 14177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 14277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void onCharacteristicChanged(BluetoothGatt gatt, 14377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan BluetoothGattCharacteristic characteristic) { 144452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer 145452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer if (Log.isLoggable(TAG, Log.DEBUG)) { 146452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer Log.d(TAG, "onCharacteristicChanged: " + Utils.getLong(characteristic.getValue())); 147452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer } 14877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 14977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan if (characteristic.getUuid().equals(mEnrolmentTokenHandle.getUuid())) { 15077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan // Store the new token handle that the BLE server is sending us. This required 15177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan // to unlock the device. 15277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan long handle = Utils.getLong(characteristic.getValue()); 15377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan storeHandle(handle); 15477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan appendOutputText("Token handle received: " + handle); 15577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 15677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 15777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 15877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 15977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void onServiceDiscovered(BluetoothGattService service) { 16077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan if (!service.getUuid().equals(mEnrolmentServiceUuid.getUuid())) { 161452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer if (Log.isLoggable(TAG, Log.DEBUG)) { 162452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer Log.d(TAG, "Service UUID: " + service.getUuid() 163452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer + " does not match Enrolment UUID " + mEnrolmentServiceUuid.getUuid()); 164452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer } 16577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan return; 16677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 16777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 168452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer if (Log.isLoggable(TAG, Log.DEBUG)) { 169452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer Log.d(TAG, "Enrolment Service # characteristics: " 170452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer + service.getCharacteristics().size()); 171452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer } 17277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolmentEscrowToken 173452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer = Utils.getCharacteristic(R.string.enrollment_token_uuid, service, mContext); 17477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolmentTokenHandle 175452ad74f4c25e153c00d94c3db674deab8efb1acRakesh Iyer = Utils.getCharacteristic(R.string.enrollment_handle_uuid, service, mContext); 17677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mClient.setCharacteristicNotification(mEnrolmentTokenHandle, true /* enable */); 17777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan appendOutputText("Enrolment BLE client successfully connected"); 17877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 17977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mHandler.post(new Runnable() { 18077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 18177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void run() { 18277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan // Services are now set up, allow users to enrol new escrow tokens. 18377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolButton.setEnabled(true); 18477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mEnrolButton.setAlpha(1.0f); 18577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 18677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan }); 18777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 18877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan }; 18977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 19077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private void storeHandle(long handle) { 19177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 19277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan prefs.edit().putLong(mTokenHandleKey, handle).apply(); 19377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 19477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 19577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private void storeToken(byte[] token) { 19677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 19777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan String byteArray = Base64.encodeToString(token, Base64.DEFAULT); 19877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan prefs.edit().putString(mEscrowTokenKey, byteArray).apply(); 19977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 20077e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan 20177e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan private void appendOutputText(final String text) { 20277e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mHandler.post(new Runnable() { 20377e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan @Override 20477e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan public void run() { 20577e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan mTextView.append("\n" + text); 20677e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 20777e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan }); 20877e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan } 20977e5e49cf9dcceb69b07510c380ae2a9285ebfeeVictor Chan} 210