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