1/*
2 * Copyright (C) 2016 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 */
16package com.android.bluetooth.pbapclient;
17
18import android.accounts.Account;
19import android.accounts.AccountManager;
20import android.app.Service;
21import android.bluetooth.BluetoothAdapter;
22import android.bluetooth.BluetoothDevice;
23import android.bluetooth.BluetoothProfile;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.Intent;
27import android.os.AsyncTask;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.HandlerThread;
31import android.os.IBinder;
32import android.os.Looper;
33import android.os.Message;
34import android.os.Process;
35import android.net.Uri;
36import android.provider.CallLog;
37import android.provider.ContactsContract;
38import android.util.Log;
39import android.util.Pair;
40
41import com.android.vcard.VCardEntry;
42import com.android.bluetooth.btservice.ProfileService;
43import com.android.bluetooth.R;
44
45import java.util.ArrayDeque;
46import java.util.ArrayList;
47import java.util.List;
48import java.util.Queue;
49import java.lang.InterruptedException;
50import java.lang.Thread;
51/**
52 * These are the possible paths that can be pulled:
53 *       BluetoothPbapClient.PB_PATH;
54 *       BluetoothPbapClient.SIM_PB_PATH;
55 *       BluetoothPbapClient.ICH_PATH;
56 *       BluetoothPbapClient.SIM_ICH_PATH;
57 *       BluetoothPbapClient.OCH_PATH;
58 *       BluetoothPbapClient.SIM_OCH_PATH;
59 *       BluetoothPbapClient.MCH_PATH;
60 *       BluetoothPbapClient.SIM_MCH_PATH;
61 */
62public class PbapPCEClient  implements PbapHandler.PbapListener {
63    private static final String TAG = "PbapPCEClient";
64    private static final boolean DBG = false;
65    private final Queue<PullRequest> mPendingRequests = new ArrayDeque<PullRequest>();
66    private BluetoothDevice mDevice;
67    private BluetoothPbapClient mClient;
68    private boolean mClientConnected = false;
69    private PbapHandler mHandler;
70    private ConnectionHandler mConnectionHandler;
71    private PullRequest mLastPull;
72    private HandlerThread mContactHandlerThread;
73    private Handler mContactHandler;
74    private Account mAccount = null;
75    private Context mContext = null;
76    private AccountManager mAccountManager;
77
78    PbapPCEClient(Context context) {
79        mContext = context;
80        mConnectionHandler = new ConnectionHandler(mContext.getMainLooper());
81        mHandler = new PbapHandler(this);
82        mAccountManager = AccountManager.get(mContext);
83        mContactHandlerThread = new HandlerThread("PBAP contact handler",
84                Process.THREAD_PRIORITY_BACKGROUND);
85        mContactHandlerThread.start();
86        mContactHandler = new ContactHandler(mContactHandlerThread.getLooper());
87    }
88
89    public int getConnectionState() {
90        if (mDevice == null) {
91            return BluetoothProfile.STATE_DISCONNECTED;
92        }
93        BluetoothPbapClient.ConnectionState currentState = mClient.getState();
94        int bluetoothConnectionState;
95        switch(currentState) {
96          case DISCONNECTED:
97              bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTED;
98              break;
99          case CONNECTING:
100              bluetoothConnectionState = BluetoothProfile.STATE_CONNECTING;
101              break;
102          case CONNECTED:
103              bluetoothConnectionState = BluetoothProfile.STATE_CONNECTED;
104              break;
105          case DISCONNECTING:
106              bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTING;
107              break;
108          default:
109              bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTED;
110        }
111        return bluetoothConnectionState;
112    }
113
114    public BluetoothDevice getDevice() {
115        return mDevice;
116    }
117
118    private boolean processNextRequest() {
119        if (DBG) {
120            Log.d(TAG,"processNextRequest()");
121        }
122        if (mPendingRequests.isEmpty()) {
123            return false;
124        }
125        if (mClient != null  && mClient.getState() ==
126                BluetoothPbapClient.ConnectionState.CONNECTED) {
127            mLastPull = mPendingRequests.remove();
128            if (DBG) {
129                Log.d(TAG, "Pulling phone book from: " + mLastPull.path);
130            }
131            return mClient.pullPhoneBook(mLastPull.path);
132        }
133        return false;
134    }
135
136
137    @Override
138    public void onPhoneBookPullDone(List<VCardEntry> entries) {
139        mLastPull.setResults(entries);
140        mContactHandler.obtainMessage(ContactHandler.EVENT_ADD_CONTACTS,mLastPull).sendToTarget();
141        processNextRequest();
142    }
143
144    @Override
145    public void onPhoneBookError() {
146        if (DBG) {
147            Log.d(TAG, "Error, mLastPull = "  + mLastPull);
148        }
149        processNextRequest();
150    }
151
152    @Override
153    public synchronized void onPbapClientConnected(boolean status) {
154        mClientConnected = status;
155        if (mClientConnected == false) {
156            // If we are disconnected then whatever the current device is we should simply clean up.
157            onConnectionStateChanged(mDevice, BluetoothProfile.STATE_CONNECTING,
158                    BluetoothProfile.STATE_DISCONNECTED);
159            disconnect(null);
160        }
161        if (mClientConnected == true) {
162            onConnectionStateChanged(mDevice, BluetoothProfile.STATE_CONNECTING,
163                    BluetoothProfile.STATE_CONNECTED);
164            processNextRequest();
165        }
166    }
167
168    private class ConnectionHandler extends Handler {
169        public static final int EVENT_CONNECT = 1;
170        public static final int EVENT_DISCONNECT = 2;
171
172        public ConnectionHandler(Looper looper) {
173            super(looper);
174        }
175
176        @Override
177        public void handleMessage(Message msg) {
178            if (DBG) {
179                Log.d(TAG, "Connection Handler Message " + msg.what + " with " + msg.obj);
180            }
181            switch (msg.what) {
182                case EVENT_CONNECT:
183                    if (msg.obj instanceof BluetoothDevice) {
184                        BluetoothDevice device = (BluetoothDevice) msg.obj;
185                        int oldState = getConnectionState();
186                        if (oldState != BluetoothProfile.STATE_DISCONNECTED) {
187                            return;
188                        }
189                        handleConnect(device);
190                    } else {
191                        Log.e(TAG, "Invalid instance in Connection Handler:Connect");
192                    }
193                    break;
194
195                case EVENT_DISCONNECT:
196                    if (mDevice == null) {
197                        return;
198                    }
199                    if (msg.obj == null || msg.obj instanceof BluetoothDevice) {
200                        BluetoothDevice device = (BluetoothDevice) msg.obj;
201                        if (!mDevice.equals(device)) {
202                            return;
203                        }
204                        int oldState = getConnectionState();
205                        handleDisconnect(device);
206                        int newState = getConnectionState();
207                        if (device != null) {
208                            onConnectionStateChanged(device, oldState, newState);
209                        }
210                    } else {
211                        Log.e(TAG, "Invalid instance in Connection Handler:Disconnect");
212                    }
213                    break;
214
215                default:
216                    Log.e(TAG, "Unknown Request to Connection Handler");
217                    break;
218            }
219        }
220
221        private void handleConnect(BluetoothDevice device) {
222          Log.d(TAG,"HANDLECONNECT" + device);
223            if (device == null) {
224                throw new IllegalStateException(TAG + ":Connect with null device!");
225            } else if (mDevice != null && !mDevice.equals(device)) {
226                // Check that we are not already connected to an existing different device.
227                // Since the device can be connected to multiple external devices -- we use the honor
228                // protocol and only accept the first connecting device.
229                Log.e(TAG, ":Got a connected event when connected to a different device. " +
230                      "existing = " + mDevice + " new = " + device);
231                return;
232            } else if (device.equals(mDevice)) {
233                Log.w(TAG, "Got a connected event for the same device. Ignoring!");
234                return;
235            }
236            // Update the device.
237            mDevice = device;
238            onConnectionStateChanged(mDevice,BluetoothProfile.STATE_DISCONNECTED,
239                    BluetoothProfile.STATE_CONNECTING);
240            // Add the account. This should give us a place to stash the data.
241            mAccount = new Account(device.getAddress(),
242                    mContext.getString(R.string.pbap_account_type));
243            mContactHandler.obtainMessage(ContactHandler.EVENT_ADD_ACCOUNT, mAccount)
244                    .sendToTarget();
245            mClient = new BluetoothPbapClient(mDevice, mAccount, mHandler);
246            downloadPhoneBook();
247            downloadCallLogs();
248            mClient.connect();
249        }
250
251        private void handleDisconnect(BluetoothDevice device) {
252            Log.w(TAG, "pbap disconnecting from = " + device);
253
254            if (device == null) {
255                // If we have a null device then disconnect the current device.
256                device = mDevice;
257            } else if (mDevice == null) {
258                Log.w(TAG, "No existing device connected to service - ignoring device = " + device);
259                return;
260            } else if (!mDevice.equals(device)) {
261                Log.w(TAG, "Existing device different from disconnected device. existing = " + mDevice +
262                           " disconnecting device = " + device);
263                return;
264            }
265            resetState();
266        }
267    }
268
269    private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
270        Intent intent = new Intent(android.bluetooth.BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
271        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
272        intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
273        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
274        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
275        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
276        Log.d(TAG,"Connection state " + device + ": " + prevState + "->" + state);
277    }
278
279    public void connect(BluetoothDevice device) {
280        mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_CONNECT,device).sendToTarget();
281    }
282
283    public void disconnect(BluetoothDevice device) {
284        mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_DISCONNECT,device).sendToTarget();
285    }
286
287    public void start() {
288        if (mDevice != null) {
289            // We are already connected -Ignore.
290            Log.w(TAG, "Already started, ignoring request to start again.");
291            return;
292        }
293        // Device is NULL, we go on remove any unclean shutdown accounts.
294        mContactHandler.obtainMessage(ContactHandler.EVENT_CLEANUP).sendToTarget();
295    }
296
297    private void resetState() {
298        if (DBG) {
299            Log.d(TAG,"resetState()");
300        }
301        if (mClient != null) {
302            // This should abort any inflight messages.
303            mClient.disconnect();
304        }
305        mClient = null;
306        mClientConnected = false;
307
308        mContactHandler.removeCallbacksAndMessages(null);
309        mContactHandlerThread.interrupt();
310        mContactHandler.obtainMessage(ContactHandler.EVENT_CLEANUP).sendToTarget();
311
312        mDevice = null;
313        mAccount = null;
314        mPendingRequests.clear();
315        if (DBG) {
316            Log.d(TAG,"resetState Complete");
317        }
318
319    }
320
321    private void downloadCallLogs() {
322        // Download Incoming Call Logs.
323        CallLogPullRequest ichCallLog =
324                new CallLogPullRequest(mContext, BluetoothPbapClient.ICH_PATH);
325        addPullRequest(ichCallLog);
326
327        // Downoad Outgoing Call Logs.
328        CallLogPullRequest ochCallLog =
329                new CallLogPullRequest(mContext, BluetoothPbapClient.OCH_PATH);
330        addPullRequest(ochCallLog);
331
332        // Downoad Missed Call Logs.
333        CallLogPullRequest mchCallLog =
334                new CallLogPullRequest(mContext, BluetoothPbapClient.MCH_PATH);
335        addPullRequest(mchCallLog);
336    }
337
338    private void downloadPhoneBook() {
339        // Download the phone book.
340        PhonebookPullRequest pb = new PhonebookPullRequest(mContext, mAccount);
341        addPullRequest(pb);
342    }
343
344    private void addPullRequest(PullRequest r) {
345        if (DBG) {
346            Log.d(TAG, "pull request mClient=" + mClient + " connected= " +
347                    mClientConnected + " mDevice=" + mDevice + " path= " + r.path);
348        }
349        if (mClient == null || mDevice == null) {
350            // It seems we want to pull but the bt connection isn't up, fail it
351            // immediately.
352            Log.w(TAG, "aborting pull request.");
353            return;
354        }
355        mPendingRequests.add(r);
356    }
357
358    private class ContactHandler extends Handler {
359        public static final int EVENT_ADD_ACCOUNT = 1;
360        public static final int EVENT_ADD_CONTACTS = 2;
361        public static final int EVENT_CLEANUP = 3;
362
363        public ContactHandler(Looper looper) {
364          super(looper);
365        }
366
367        @Override
368        public void handleMessage(Message msg) {
369            if (DBG) {
370                Log.d(TAG, "Contact Handler Message " + msg.what + " with " + msg.obj);
371            }
372            switch (msg.what) {
373                case EVENT_ADD_ACCOUNT:
374                    if (msg.obj instanceof Account) {
375                        Account account = (Account) msg.obj;
376                        addAccount(account);
377                    } else {
378                        Log.e(TAG, "invalid Instance in Contact Handler: Add Account");
379                    }
380                    break;
381
382                case EVENT_ADD_CONTACTS:
383                    if (msg.obj instanceof PullRequest) {
384                        PullRequest req = (PullRequest) msg.obj;
385                        req.onPullComplete();
386                    } else {
387                        Log.e(TAG, "invalid Instance in Contact Handler: Add Contacts");
388                    }
389                    break;
390
391                case EVENT_CLEANUP:
392                    Thread.currentThread().interrupted();  //clear state of interrupt.
393                    removeUncleanAccounts();
394                    mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, null, null);
395                    if (DBG) {
396                        Log.d(TAG, "Call logs deleted.");
397                    }
398                    break;
399
400                default:
401                    Log.e(TAG, "Unknown Request to Contact Handler");
402                    break;
403            }
404        }
405
406        private void removeUncleanAccounts() {
407            // Find all accounts that match the type "pbap" and delete them. This section is
408            // executed only if the device was shut down in an unclean state and contacts persisted.
409            Account[] accounts =
410                mAccountManager.getAccountsByType(mContext.getString(R.string.pbap_account_type));
411            Log.w(TAG, "Found " + accounts.length + " unclean accounts");
412            for (Account acc : accounts) {
413                Log.w(TAG, "Deleting " + acc);
414                // The device ID is the name of the account.
415                removeAccount(acc);
416            }
417        }
418
419        private boolean addAccount(Account account) {
420            if (mAccountManager.addAccountExplicitly(account, null, null)) {
421                if (DBG) {
422                    Log.d(TAG, "Added account " + mAccount);
423                }
424                return true;
425            }
426            return false;
427        }
428
429        private boolean removeAccount(Account acc) {
430            if (mAccountManager.removeAccountExplicitly(acc)) {
431                if (DBG) {
432                    Log.d(TAG, "Removed account " + acc);
433                }
434                return true;
435            }
436            Log.e(TAG, "Failed to remove account " + mAccount);
437            return false;
438        }
439   }
440}
441