• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2008-2009, Motorola, Inc.
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * - Redistributions of source code must retain the above copyright notice,
10  * this list of conditions and the following disclaimer.
11  *
12  * - Redistributions in binary form must reproduce the above copyright notice,
13  * this list of conditions and the following disclaimer in the documentation
14  * and/or other materials provided with the distribution.
15  *
16  * - Neither the name of the Motorola, Inc. nor the names of its contributors
17  * may be used to endorse or promote products derived from this software
18  * without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 package com.android.bluetooth.pbap;
34 
35 import android.content.Context;
36 import android.content.ContentResolver;
37 import android.database.Cursor;
38 import android.os.Message;
39 import android.os.Handler;
40 import android.provider.CallLog.Calls;
41 import android.provider.CallLog;
42 import android.text.TextUtils;
43 import android.util.Log;
44 
45 import java.io.IOException;
46 import java.io.OutputStream;
47 import java.text.CharacterIterator;
48 import java.text.StringCharacterIterator;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 
52 import javax.obex.ServerRequestHandler;
53 import javax.obex.ResponseCodes;
54 import javax.obex.ApplicationParameter;
55 import javax.obex.ServerOperation;
56 import javax.obex.Operation;
57 import javax.obex.HeaderSet;
58 
59 public class BluetoothPbapObexServer extends ServerRequestHandler {
60 
61     private static final String TAG = "BluetoothPbapObexServer";
62 
63     private static final boolean D = BluetoothPbapService.DEBUG;
64 
65     private static final boolean V = BluetoothPbapService.VERBOSE;
66 
67     private static final int UUID_LENGTH = 16;
68 
69     // The length of suffix of vcard name - ".vcf" is 5
70     private static final int VCARD_NAME_SUFFIX_LENGTH = 5;
71 
72     // 128 bit UUID for PBAP
73     private static final byte[] PBAP_TARGET = new byte[] {
74             0x79, 0x61, 0x35, (byte)0xf0, (byte)0xf0, (byte)0xc5, 0x11, (byte)0xd8, 0x09, 0x66,
75             0x08, 0x00, 0x20, 0x0c, (byte)0x9a, 0x66
76     };
77 
78     // Currently not support SIM card
79     private static final String[] LEGAL_PATH = {
80             "/telecom", "/telecom/pb", "/telecom/ich", "/telecom/och", "/telecom/mch",
81             "/telecom/cch"
82     };
83 
84     @SuppressWarnings("unused")
85     private static final String[] LEGAL_PATH_WITH_SIM = {
86             "/telecom", "/telecom/pb", "/telecom/ich", "/telecom/och", "/telecom/mch",
87             "/telecom/cch", "/SIM1", "/SIM1/telecom", "/SIM1/telecom/ich", "/SIM1/telecom/och",
88             "/SIM1/telecom/mch", "/SIM1/telecom/cch", "/SIM1/telecom/pb"
89 
90     };
91 
92     // SIM card
93     private static final String SIM1 = "SIM1";
94 
95     // missed call history
96     private static final String MCH = "mch";
97 
98     // incoming call history
99     private static final String ICH = "ich";
100 
101     // outgoing call history
102     private static final String OCH = "och";
103 
104     // combined call history
105     private static final String CCH = "cch";
106 
107     // phone book
108     private static final String PB = "pb";
109 
110     private static final String TELECOM_PATH = "/telecom";
111 
112     private static final String ICH_PATH = "/telecom/ich";
113 
114     private static final String OCH_PATH = "/telecom/och";
115 
116     private static final String MCH_PATH = "/telecom/mch";
117 
118     private static final String CCH_PATH = "/telecom/cch";
119 
120     private static final String PB_PATH = "/telecom/pb";
121 
122     // type for list vcard objects
123     private static final String TYPE_LISTING = "x-bt/vcard-listing";
124 
125     // type for get single vcard object
126     private static final String TYPE_VCARD = "x-bt/vcard";
127 
128     // to indicate if need send body besides headers
129     private static final int NEED_SEND_BODY = -1;
130 
131     // type for download all vcard objects
132     private static final String TYPE_PB = "x-bt/phonebook";
133 
134     // The number of indexes in the phone book.
135     private boolean mNeedPhonebookSize = false;
136 
137     // The number of missed calls that have not been checked on the PSE at the
138     // point of the request. Only apply to "mch" case.
139     private boolean mNeedNewMissedCallsNum = false;
140 
141     private int mMissedCallSize = 0;
142 
143     // record current path the client are browsing
144     private String mCurrentPath = "";
145 
146     private Handler mCallback = null;
147 
148     private Context mContext;
149 
150     private BluetoothPbapVcardManager mVcardManager;
151 
152     private int mOrderBy  = ORDER_BY_INDEXED;
153 
154     private static int CALLLOG_NUM_LIMIT = 50;
155 
156     public static int ORDER_BY_INDEXED = 0;
157 
158     public static int ORDER_BY_ALPHABETICAL = 1;
159 
160     public static boolean sIsAborted = false;
161 
162     public static class ContentType {
163         public static final int PHONEBOOK = 1;
164 
165         public static final int INCOMING_CALL_HISTORY = 2;
166 
167         public static final int OUTGOING_CALL_HISTORY = 3;
168 
169         public static final int MISSED_CALL_HISTORY = 4;
170 
171         public static final int COMBINED_CALL_HISTORY = 5;
172     }
173 
BluetoothPbapObexServer(Handler callback, Context context)174     public BluetoothPbapObexServer(Handler callback, Context context) {
175         super();
176         mCallback = callback;
177         mContext = context;
178         mVcardManager = new BluetoothPbapVcardManager(mContext);
179     }
180 
181     @Override
onConnect(final HeaderSet request, HeaderSet reply)182     public int onConnect(final HeaderSet request, HeaderSet reply) {
183         if (V) logHeader(request);
184         notifyUpdateWakeLock();
185         try {
186             byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET);
187             if (uuid == null) {
188                 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
189             }
190             if (D) Log.d(TAG, "onConnect(): uuid=" + Arrays.toString(uuid));
191 
192             if (uuid.length != UUID_LENGTH) {
193                 Log.w(TAG, "Wrong UUID length");
194                 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
195             }
196             for (int i = 0; i < UUID_LENGTH; i++) {
197                 if (uuid[i] != PBAP_TARGET[i]) {
198                     Log.w(TAG, "Wrong UUID");
199                     return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
200                 }
201             }
202             reply.setHeader(HeaderSet.WHO, uuid);
203         } catch (IOException e) {
204             Log.e(TAG, e.toString());
205             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
206         }
207 
208         try {
209             byte[] remote = (byte[])request.getHeader(HeaderSet.WHO);
210             if (remote != null) {
211                 if (D) Log.d(TAG, "onConnect(): remote=" + Arrays.toString(remote));
212                 reply.setHeader(HeaderSet.TARGET, remote);
213             }
214         } catch (IOException e) {
215             Log.e(TAG, e.toString());
216             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
217         }
218 
219         if (V) Log.v(TAG, "onConnect(): uuid is ok, will send out " +
220                 "MSG_SESSION_ESTABLISHED msg.");
221 
222         Message msg = Message.obtain(mCallback);
223         msg.what = BluetoothPbapService.MSG_SESSION_ESTABLISHED;
224         msg.sendToTarget();
225 
226         return ResponseCodes.OBEX_HTTP_OK;
227     }
228 
229     @Override
onDisconnect(final HeaderSet req, final HeaderSet resp)230     public void onDisconnect(final HeaderSet req, final HeaderSet resp) {
231         if (D) Log.d(TAG, "onDisconnect(): enter");
232         if (V) logHeader(req);
233         notifyUpdateWakeLock();
234         resp.responseCode = ResponseCodes.OBEX_HTTP_OK;
235         if (mCallback != null) {
236             Message msg = Message.obtain(mCallback);
237             msg.what = BluetoothPbapService.MSG_SESSION_DISCONNECTED;
238             msg.sendToTarget();
239             if (V) Log.v(TAG, "onDisconnect(): msg MSG_SESSION_DISCONNECTED sent out.");
240         }
241     }
242 
243     @Override
onAbort(HeaderSet request, HeaderSet reply)244     public int onAbort(HeaderSet request, HeaderSet reply) {
245         if (D) Log.d(TAG, "onAbort(): enter.");
246         notifyUpdateWakeLock();
247         sIsAborted = true;
248         return ResponseCodes.OBEX_HTTP_OK;
249     }
250 
251     @Override
onPut(final Operation op)252     public int onPut(final Operation op) {
253         if (D) Log.d(TAG, "onPut(): not support PUT request.");
254         notifyUpdateWakeLock();
255         return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
256     }
257 
258     @Override
onDelete(final HeaderSet request, final HeaderSet reply)259     public int onDelete(final HeaderSet request, final HeaderSet reply) {
260         if (D) Log.d(TAG, "onDelete(): not support PUT request.");
261         notifyUpdateWakeLock();
262         return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
263     }
264 
265     @Override
onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup, final boolean create)266     public int onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup,
267             final boolean create) {
268         if (V) logHeader(request);
269         if (D) Log.d(TAG, "before setPath, mCurrentPath ==  " + mCurrentPath);
270         notifyUpdateWakeLock();
271         String current_path_tmp = mCurrentPath;
272         String tmp_path = null;
273         try {
274             tmp_path = (String)request.getHeader(HeaderSet.NAME);
275         } catch (IOException e) {
276             Log.e(TAG, "Get name header fail");
277             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
278         }
279         if (D) Log.d(TAG, "backup=" + backup + " create=" + create + " name=" + tmp_path);
280 
281         if (backup) {
282             if (current_path_tmp.length() != 0) {
283                 current_path_tmp = current_path_tmp.substring(0,
284                         current_path_tmp.lastIndexOf("/"));
285             }
286         } else {
287             if (tmp_path == null) {
288                 current_path_tmp = "";
289             } else {
290                 current_path_tmp = current_path_tmp + "/" + tmp_path;
291             }
292         }
293 
294         if ((current_path_tmp.length() != 0) && (!isLegalPath(current_path_tmp))) {
295             if (create) {
296                 Log.w(TAG, "path create is forbidden!");
297                 return ResponseCodes.OBEX_HTTP_FORBIDDEN;
298             } else {
299                 Log.w(TAG, "path is not legal");
300                 return ResponseCodes.OBEX_HTTP_NOT_FOUND;
301             }
302         }
303         mCurrentPath = current_path_tmp;
304         if (V) Log.v(TAG, "after setPath, mCurrentPath ==  " + mCurrentPath);
305 
306         return ResponseCodes.OBEX_HTTP_OK;
307     }
308 
309     @Override
onClose()310     public void onClose() {
311         if (mCallback != null) {
312             Message msg = Message.obtain(mCallback);
313             msg.what = BluetoothPbapService.MSG_SERVERSESSION_CLOSE;
314             msg.sendToTarget();
315             if (D) Log.d(TAG, "onClose(): msg MSG_SERVERSESSION_CLOSE sent out.");
316         }
317     }
318 
319     @Override
onGet(Operation op)320     public int onGet(Operation op) {
321         notifyUpdateWakeLock();
322         sIsAborted = false;
323         HeaderSet request = null;
324         HeaderSet reply = new HeaderSet();
325         String type = "";
326         String name = "";
327         byte[] appParam = null;
328         AppParamValue appParamValue = new AppParamValue();
329         try {
330             request = op.getReceivedHeader();
331             type = (String)request.getHeader(HeaderSet.TYPE);
332             name = (String)request.getHeader(HeaderSet.NAME);
333             appParam = (byte[])request.getHeader(HeaderSet.APPLICATION_PARAMETER);
334         } catch (IOException e) {
335             Log.e(TAG, "request headers error");
336             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
337         }
338 
339         if (V) logHeader(request);
340         if (D) Log.d(TAG, "OnGet type is " + type + "; name is " + name);
341 
342         if (type == null) {
343             return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
344         }
345         // Accroding to specification,the name header could be omitted such as
346         // sony erriccsonHBH-DS980
347 
348         // For "x-bt/phonebook" and "x-bt/vcard-listing":
349         // if name == null, guess what carkit actually want from current path
350         // For "x-bt/vcard":
351         // We decide which kind of content client would like per current path
352 
353         boolean validName = true;
354         if (TextUtils.isEmpty(name)) {
355             validName = false;
356         }
357 
358         if (!validName || (validName && type.equals(TYPE_VCARD))) {
359             if (D) Log.d(TAG, "Guess what carkit actually want from current path (" +
360                     mCurrentPath + ")");
361 
362             if (mCurrentPath.equals(PB_PATH)) {
363                 appParamValue.needTag = ContentType.PHONEBOOK;
364             } else if (mCurrentPath.equals(ICH_PATH)) {
365                 appParamValue.needTag = ContentType.INCOMING_CALL_HISTORY;
366             } else if (mCurrentPath.equals(OCH_PATH)) {
367                 appParamValue.needTag = ContentType.OUTGOING_CALL_HISTORY;
368             } else if (mCurrentPath.equals(MCH_PATH)) {
369                 appParamValue.needTag = ContentType.MISSED_CALL_HISTORY;
370                 mNeedNewMissedCallsNum = true;
371             } else if (mCurrentPath.equals(CCH_PATH)) {
372                 appParamValue.needTag = ContentType.COMBINED_CALL_HISTORY;
373             } else if (mCurrentPath.equals(TELECOM_PATH)) {
374                 /* PBAP 1.1.1 change */
375                 if (!validName && type.equals(TYPE_LISTING)) {
376                     Log.e(TAG, "invalid vcard listing request in default folder");
377                     return ResponseCodes.OBEX_HTTP_NOT_FOUND;
378                 }
379             } else {
380                 Log.w(TAG, "mCurrentpath is not valid path!!!");
381                 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
382             }
383             if (D) Log.v(TAG, "onGet(): appParamValue.needTag=" + appParamValue.needTag);
384         } else {
385             // Not support SIM card currently
386             if (name.contains(SIM1.subSequence(0, SIM1.length()))) {
387                 Log.w(TAG, "Not support access SIM card info!");
388                 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
389             }
390 
391             // we have weak name checking here to provide better
392             // compatibility with other devices,although unique name such as
393             // "pb.vcf" is required by SIG spec.
394             if (isNameMatchTarget(name, PB)) {
395                 appParamValue.needTag = ContentType.PHONEBOOK;
396                 if (D) Log.v(TAG, "download phonebook request");
397             } else if (isNameMatchTarget(name, ICH)) {
398                 appParamValue.needTag = ContentType.INCOMING_CALL_HISTORY;
399                 if (D) Log.v(TAG, "download incoming calls request");
400             } else if (isNameMatchTarget(name, OCH)) {
401                 appParamValue.needTag = ContentType.OUTGOING_CALL_HISTORY;
402                 if (D) Log.v(TAG, "download outgoing calls request");
403             } else if (isNameMatchTarget(name, MCH)) {
404                 appParamValue.needTag = ContentType.MISSED_CALL_HISTORY;
405                 mNeedNewMissedCallsNum = true;
406                 if (D) Log.v(TAG, "download missed calls request");
407             } else if (isNameMatchTarget(name, CCH)) {
408                 appParamValue.needTag = ContentType.COMBINED_CALL_HISTORY;
409                 if (D) Log.v(TAG, "download combined calls request");
410             } else {
411                 Log.w(TAG, "Input name doesn't contain valid info!!!");
412                 return ResponseCodes.OBEX_HTTP_NOT_FOUND;
413             }
414         }
415 
416         if ((appParam != null) && !parseApplicationParameter(appParam, appParamValue)) {
417             return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
418         }
419 
420         // listing request
421         if (type.equals(TYPE_LISTING)) {
422             return pullVcardListing(appParam, appParamValue, reply, op);
423         }
424         // pull vcard entry request
425         else if (type.equals(TYPE_VCARD)) {
426             return pullVcardEntry(appParam, appParamValue, op, name, mCurrentPath);
427         }
428         // down load phone book request
429         else if (type.equals(TYPE_PB)) {
430             return pullPhonebook(appParam, appParamValue, reply, op, name);
431         } else {
432             Log.w(TAG, "unknown type request!!!");
433             return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
434         }
435     }
436 
isNameMatchTarget(String name, String target)437     private boolean isNameMatchTarget(String name, String target) {
438         String contentTypeName = name;
439         if (contentTypeName.endsWith(".vcf")) {
440             contentTypeName = contentTypeName
441                     .substring(0, contentTypeName.length() - ".vcf".length());
442         }
443         // There is a test case: Client will send a wrong name "/telecom/pbpb".
444         // So we must use the String between '/' and '/' as a indivisible part
445         // for comparing.
446         String[] nameList = contentTypeName.split("/");
447         for (String subName : nameList) {
448             if (subName.equals(target)) {
449                 return true;
450             }
451         }
452         return false;
453     }
454 
455     /** check whether path is legal */
isLegalPath(final String str)456     private final boolean isLegalPath(final String str) {
457         if (str.length() == 0) {
458             return true;
459         }
460         for (int i = 0; i < LEGAL_PATH.length; i++) {
461             if (str.equals(LEGAL_PATH[i])) {
462                 return true;
463             }
464         }
465         return false;
466     }
467 
468     private class AppParamValue {
469         public int maxListCount;
470 
471         public int listStartOffset;
472 
473         public String searchValue;
474 
475         // Indicate which vCard parameter the search operation shall be carried
476         // out on. Can be "Name | Number | Sound", default value is "Name".
477         public String searchAttr;
478 
479         // Indicate which sorting order shall be used for the
480         // <x-bt/vcard-listing> listing object.
481         // Can be "Alphabetical | Indexed | Phonetical", default value is
482         // "Indexed".
483         public String order;
484 
485         public int needTag;
486 
487         public boolean vcard21;
488 
489         public byte[] filter;
490 
491         public boolean ignorefilter;
492 
AppParamValue()493         public AppParamValue() {
494             maxListCount = 0xFFFF;
495             listStartOffset = 0;
496             searchValue = "";
497             searchAttr = "";
498             order = "";
499             needTag = 0x00;
500             vcard21 = true;
501             //Filter is not set by default
502             ignorefilter = true;
503             filter = new byte[] {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} ;
504         }
505 
dump()506         public void dump() {
507             Log.i(TAG, "maxListCount=" + maxListCount + " listStartOffset=" + listStartOffset
508                     + " searchValue=" + searchValue + " searchAttr=" + searchAttr + " needTag="
509                     + needTag + " vcard21=" + vcard21 + " order=" + order);
510         }
511     }
512 
513     /** To parse obex application parameter */
parseApplicationParameter(final byte[] appParam, AppParamValue appParamValue)514     private final boolean parseApplicationParameter(final byte[] appParam,
515             AppParamValue appParamValue) {
516         int i = 0;
517         boolean parseOk = true;
518         while ((i < appParam.length) && (parseOk == true)) {
519             switch (appParam[i]) {
520                 case ApplicationParameter.TRIPLET_TAGID.FILTER_TAGID:
521                     i += 2; // length and tag field in triplet
522                     for (int index=0; index < ApplicationParameter.TRIPLET_LENGTH.FILTER_LENGTH;
523                          index++) {
524                         if (appParam[i+index] != 0){
525                             appParamValue.ignorefilter = false;
526                             appParamValue.filter[index] = appParam[i+index];
527                         }
528                     }
529                     i += ApplicationParameter.TRIPLET_LENGTH.FILTER_LENGTH;
530                     break;
531                 case ApplicationParameter.TRIPLET_TAGID.ORDER_TAGID:
532                     i += 2; // length and tag field in triplet
533                     appParamValue.order = Byte.toString(appParam[i]);
534                     i += ApplicationParameter.TRIPLET_LENGTH.ORDER_LENGTH;
535                     break;
536                 case ApplicationParameter.TRIPLET_TAGID.SEARCH_VALUE_TAGID:
537                     i += 1; // length field in triplet
538                     // length of search value is variable
539                     int length = appParam[i];
540                     if (length == 0) {
541                         parseOk = false;
542                         break;
543                     }
544                     if (appParam[i+length] == 0x0) {
545                         appParamValue.searchValue = new String(appParam, i + 1, length-1);
546                     } else {
547                         appParamValue.searchValue = new String(appParam, i + 1, length);
548                     }
549                     i += length;
550                     i += 1;
551                     break;
552                 case ApplicationParameter.TRIPLET_TAGID.SEARCH_ATTRIBUTE_TAGID:
553                     i += 2;
554                     appParamValue.searchAttr = Byte.toString(appParam[i]);
555                     i += ApplicationParameter.TRIPLET_LENGTH.SEARCH_ATTRIBUTE_LENGTH;
556                     break;
557                 case ApplicationParameter.TRIPLET_TAGID.MAXLISTCOUNT_TAGID:
558                     i += 2;
559                     if (appParam[i] == 0 && appParam[i + 1] == 0) {
560                         mNeedPhonebookSize = true;
561                     } else {
562                         int highValue = appParam[i] & 0xff;
563                         int lowValue = appParam[i + 1] & 0xff;
564                         appParamValue.maxListCount = highValue * 256 + lowValue;
565                     }
566                     i += ApplicationParameter.TRIPLET_LENGTH.MAXLISTCOUNT_LENGTH;
567                     break;
568                 case ApplicationParameter.TRIPLET_TAGID.LISTSTARTOFFSET_TAGID:
569                     i += 2;
570                     int highValue = appParam[i] & 0xff;
571                     int lowValue = appParam[i + 1] & 0xff;
572                     appParamValue.listStartOffset = highValue * 256 + lowValue;
573                     i += ApplicationParameter.TRIPLET_LENGTH.LISTSTARTOFFSET_LENGTH;
574                     break;
575                 case ApplicationParameter.TRIPLET_TAGID.FORMAT_TAGID:
576                     i += 2;// length field in triplet
577                     if (appParam[i] != 0) {
578                         appParamValue.vcard21 = false;
579                     }
580                     i += ApplicationParameter.TRIPLET_LENGTH.FORMAT_LENGTH;
581                     break;
582                 default:
583                     parseOk = false;
584                     Log.e(TAG, "Parse Application Parameter error");
585                     break;
586             }
587         }
588 
589         if (D) appParamValue.dump();
590 
591         return parseOk;
592     }
593 
594     /** Form and Send an XML format String to client for Phone book listing */
sendVcardListingXml(final int type, Operation op, final int maxListCount, final int listStartOffset, final String searchValue, String searchAttr)595     private final int sendVcardListingXml(final int type, Operation op,
596             final int maxListCount, final int listStartOffset, final String searchValue,
597             String searchAttr) {
598         StringBuilder result = new StringBuilder();
599         int itemsFound = 0;
600         result.append("<?xml version=\"1.0\"?>");
601         result.append("<!DOCTYPE vcard-listing SYSTEM \"vcard-listing.dtd\">");
602         result.append("<vCard-listing version=\"1.0\">");
603 
604         // Phonebook listing request
605         if (type == ContentType.PHONEBOOK) {
606             if (searchAttr.equals("0")) { // search by name
607                 itemsFound = createList(maxListCount, listStartOffset, searchValue, result,
608                         "name");
609             } else if (searchAttr.equals("1")) { // search by number
610                 itemsFound = createList(maxListCount, listStartOffset, searchValue, result,
611                         "number");
612             }// end of search by number
613             else {
614                 return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
615             }
616         }
617         // Call history listing request
618         else {
619             ArrayList<String> nameList = mVcardManager.loadCallHistoryList(type);
620             int requestSize = nameList.size() >= maxListCount ? maxListCount : nameList.size();
621             int startPoint = listStartOffset;
622             int endPoint = startPoint + requestSize;
623             if (endPoint > nameList.size()) {
624                 endPoint = nameList.size();
625             }
626             if (D) Log.d(TAG, "call log list, size=" + requestSize + " offset=" + listStartOffset);
627 
628             for (int j = startPoint; j < endPoint; j++) {
629                 writeVCardEntry(j+1, nameList.get(j),result);
630             }
631         }
632         result.append("</vCard-listing>");
633 
634         if (V) Log.v(TAG, "itemsFound =" + itemsFound);
635 
636         return pushBytes(op, result.toString());
637     }
638 
createList(final int maxListCount, final int listStartOffset, final String searchValue, StringBuilder result, String type)639     private int createList(final int maxListCount, final int listStartOffset,
640             final String searchValue, StringBuilder result, String type) {
641         int itemsFound = 0;
642         ArrayList<String> nameList = mVcardManager.getPhonebookNameList(mOrderBy);
643         final int requestSize = nameList.size() >= maxListCount ? maxListCount : nameList.size();
644         final int listSize = nameList.size();
645         String compareValue = "", currentValue;
646 
647         if (D) Log.d(TAG, "search by " + type + ", requestSize=" + requestSize + " offset="
648                     + listStartOffset + " searchValue=" + searchValue);
649 
650         if (type.equals("number")) {
651             // query the number, to get the names
652             ArrayList<String> names = mVcardManager.getContactNamesByNumber(searchValue);
653             for (int i = 0; i < names.size(); i++) {
654                 compareValue = names.get(i).trim();
655                 if (D) Log.d(TAG, "compareValue=" + compareValue);
656                 for (int pos = listStartOffset; pos < listSize &&
657                         itemsFound < requestSize; pos++) {
658                     currentValue = nameList.get(pos);
659                     if (D) Log.d(TAG, "currentValue=" + currentValue);
660                     if (currentValue.equals(compareValue)) {
661                         itemsFound++;
662                         if (currentValue.contains(","))
663                            currentValue = currentValue.substring(0, currentValue.lastIndexOf(','));
664                         writeVCardEntry(pos, currentValue,result);
665                     }
666                 }
667                 if (itemsFound >= requestSize) {
668                     break;
669                 }
670             }
671         } else {
672             if (searchValue != null) {
673                 compareValue = searchValue.trim();
674             }
675             for (int pos = listStartOffset; pos < listSize &&
676                     itemsFound < requestSize; pos++) {
677                 currentValue = nameList.get(pos);
678                 if (currentValue.contains(","))
679                     currentValue = currentValue.substring(0, currentValue.lastIndexOf(','));
680 
681                 if (searchValue.isEmpty() || ((currentValue.toLowerCase()).equals(compareValue.toLowerCase()))) {
682                     itemsFound++;
683                     writeVCardEntry(pos, currentValue,result);
684                 }
685             }
686         }
687         return itemsFound;
688     }
689 
690     /**
691      * Function to send obex header back to client such as get phonebook size
692      * request
693      */
pushHeader(final Operation op, final HeaderSet reply)694     private final int pushHeader(final Operation op, final HeaderSet reply) {
695         OutputStream outputStream = null;
696 
697         if (D) Log.d(TAG, "Push Header");
698         if (D) Log.d(TAG, reply.toString());
699 
700         int pushResult = ResponseCodes.OBEX_HTTP_OK;
701         try {
702             op.sendHeaders(reply);
703             outputStream = op.openOutputStream();
704             outputStream.flush();
705         } catch (IOException e) {
706             Log.e(TAG, e.toString());
707             pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
708         } finally {
709             if (!closeStream(outputStream, op)) {
710                 pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
711             }
712         }
713         return pushResult;
714     }
715 
716     /** Function to send vcard data to client */
pushBytes(Operation op, final String vcardString)717     private final int pushBytes(Operation op, final String vcardString) {
718         if (vcardString == null) {
719             Log.w(TAG, "vcardString is null!");
720             return ResponseCodes.OBEX_HTTP_OK;
721         }
722 
723         OutputStream outputStream = null;
724         int pushResult = ResponseCodes.OBEX_HTTP_OK;
725         try {
726             outputStream = op.openOutputStream();
727             outputStream.write(vcardString.getBytes());
728             if (V) Log.v(TAG, "Send Data complete!");
729         } catch (IOException e) {
730             Log.e(TAG, "open/write outputstrem failed" + e.toString());
731             pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
732         }
733 
734         if (!closeStream(outputStream, op)) {
735             pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
736         }
737 
738         return pushResult;
739     }
740 
handleAppParaForResponse(AppParamValue appParamValue, int size, HeaderSet reply, Operation op)741     private final int handleAppParaForResponse(AppParamValue appParamValue, int size,
742             HeaderSet reply, Operation op) {
743         byte[] misnum = new byte[1];
744         ApplicationParameter ap = new ApplicationParameter();
745 
746         // In such case, PCE only want the number of index.
747         // So response not contain any Body header.
748         if (mNeedPhonebookSize) {
749             if (V) Log.v(TAG, "Need Phonebook size in response header.");
750             mNeedPhonebookSize = false;
751 
752             byte[] pbsize = new byte[2];
753 
754             pbsize[0] = (byte)((size / 256) & 0xff);// HIGH VALUE
755             pbsize[1] = (byte)((size % 256) & 0xff);// LOW VALUE
756             ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.PHONEBOOKSIZE_TAGID,
757                     ApplicationParameter.TRIPLET_LENGTH.PHONEBOOKSIZE_LENGTH, pbsize);
758 
759             if (mNeedNewMissedCallsNum) {
760                 mNeedNewMissedCallsNum = false;
761                 int nmnum = 0;
762                 ContentResolver contentResolver;
763                 contentResolver = mContext.getContentResolver();
764 
765                 Cursor c = contentResolver.query(
766                     Calls.CONTENT_URI,
767                     null,
768                     Calls.TYPE + " = " + Calls.MISSED_TYPE + " AND " + android.provider.CallLog.Calls.NEW + " = 1",
769                     null,
770                     Calls.DEFAULT_SORT_ORDER);
771 
772                 if (c != null) {
773                   nmnum = c.getCount();
774                   c.close();
775                 }
776 
777                 nmnum = nmnum > 0 ? nmnum : 0;
778                 misnum[0] = (byte)nmnum;
779                 if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true,  num= " + nmnum);
780             }
781             reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam());
782 
783             if (D) Log.d(TAG, "Send back Phonebook size only, without body info! Size= " + size);
784 
785             return pushHeader(op, reply);
786         }
787 
788         // Only apply to "mch" download/listing.
789         // NewMissedCalls is used only in the response, together with Body
790         // header.
791         if (mNeedNewMissedCallsNum) {
792             if (V) Log.v(TAG, "Need new missed call num in response header.");
793             mNeedNewMissedCallsNum = false;
794             int nmnum = 0;
795             ContentResolver contentResolver;
796             contentResolver = mContext.getContentResolver();
797 
798             Cursor c = contentResolver.query(
799                 Calls.CONTENT_URI,
800                 null,
801                 Calls.TYPE + " = " + Calls.MISSED_TYPE + " AND " + android.provider.CallLog.Calls.NEW + " = 1",
802                 null,
803                 Calls.DEFAULT_SORT_ORDER);
804 
805             if (c != null) {
806               nmnum = c.getCount();
807               c.close();
808             }
809 
810             nmnum = nmnum > 0 ? nmnum : 0;
811             misnum[0] = (byte)nmnum;
812             if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true,  num= " + nmnum);
813 
814             ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID,
815                     ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum);
816             reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam());
817             if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true,  num= "
818                         + nmnum);
819 
820             // Only Specifies the headers, not write for now, will write to PCE
821             // together with Body
822             try {
823                 op.sendHeaders(reply);
824             } catch (IOException e) {
825                 Log.e(TAG, e.toString());
826                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
827             }
828         }
829         return NEED_SEND_BODY;
830     }
831 
pullVcardListing(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, Operation op)832     private final int pullVcardListing(byte[] appParam, AppParamValue appParamValue,
833             HeaderSet reply, Operation op) {
834         String searchAttr = appParamValue.searchAttr.trim();
835 
836         if (searchAttr == null || searchAttr.length() == 0) {
837             // If searchAttr is not set by PCE, set default value per spec.
838             appParamValue.searchAttr = "0";
839             if (D) Log.d(TAG, "searchAttr is not set by PCE, assume search by name by default");
840         } else if (!searchAttr.equals("0") && !searchAttr.equals("1")) {
841             Log.w(TAG, "search attr not supported");
842             if (searchAttr.equals("2")) {
843                 // search by sound is not supported currently
844                 Log.w(TAG, "do not support search by sound");
845                 return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
846             }
847             return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
848         } else {
849             Log.i(TAG, "searchAttr is valid: " + searchAttr);
850         }
851 
852         int size = mVcardManager.getPhonebookSize(appParamValue.needTag);
853         int needSendBody = handleAppParaForResponse(appParamValue, size, reply, op);
854         if (needSendBody != NEED_SEND_BODY) {
855             return needSendBody;
856         }
857 
858         if (size == 0) {
859             if (V) Log.v(TAG, "PhonebookSize is 0, return.");
860             return ResponseCodes.OBEX_HTTP_OK;
861         }
862 
863         String orderPara = appParamValue.order.trim();
864         if (TextUtils.isEmpty(orderPara)) {
865             // If order parameter is not set by PCE, set default value per spec.
866             orderPara = "0";
867             if (D) Log.d(TAG, "Order parameter is not set by PCE. " +
868                        "Assume order by 'Indexed' by default");
869         } else if (!orderPara.equals("0") && !orderPara.equals("1")) {
870             if (V) Log.v(TAG, "Order parameter is not supported: " + appParamValue.order);
871             if (orderPara.equals("2")) {
872                 // Order by sound is not supported currently
873                 Log.w(TAG, "Do not support order by sound");
874                 return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
875             }
876             return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
877         } else {
878             Log.i(TAG, "Order parameter is valid: " + orderPara);
879         }
880 
881         if (orderPara.equals("0")) {
882             mOrderBy = ORDER_BY_INDEXED;
883         } else if (orderPara.equals("1")) {
884             mOrderBy = ORDER_BY_ALPHABETICAL;
885         }
886 
887         int sendResult = sendVcardListingXml(appParamValue.needTag, op, appParamValue.maxListCount,
888                 appParamValue.listStartOffset, appParamValue.searchValue,
889                 appParamValue.searchAttr);
890         return sendResult;
891     }
892 
pullVcardEntry(byte[] appParam, AppParamValue appParamValue, Operation op, final String name, final String current_path)893     private final int pullVcardEntry(byte[] appParam, AppParamValue appParamValue,
894             Operation op, final String name, final String current_path) {
895         if (name == null || name.length() < VCARD_NAME_SUFFIX_LENGTH) {
896             if (D) Log.d(TAG, "Name is Null, or the length of name < 5 !");
897             return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
898         }
899         String strIndex = name.substring(0, name.length() - VCARD_NAME_SUFFIX_LENGTH + 1);
900         int intIndex = 0;
901         if (strIndex.trim().length() != 0) {
902             try {
903                 intIndex = Integer.parseInt(strIndex);
904             } catch (NumberFormatException e) {
905                 Log.e(TAG, "catch number format exception " + e.toString());
906                 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
907             }
908         }
909 
910         int size = mVcardManager.getPhonebookSize(appParamValue.needTag);
911         if (size == 0) {
912             if (V) Log.v(TAG, "PhonebookSize is 0, return.");
913             return ResponseCodes.OBEX_HTTP_NOT_FOUND;
914         }
915 
916         boolean vcard21 = appParamValue.vcard21;
917         if (appParamValue.needTag == 0) {
918             Log.w(TAG, "wrong path!");
919             return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
920         } else if (appParamValue.needTag == ContentType.PHONEBOOK) {
921             if (intIndex < 0 || intIndex >= size) {
922                 Log.w(TAG, "The requested vcard is not acceptable! name= " + name);
923                 return ResponseCodes.OBEX_HTTP_NOT_FOUND;
924             } else if (intIndex == 0) {
925                 // For PB_PATH, 0.vcf is the phone number of this phone.
926                 String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21,null);
927                 return pushBytes(op, ownerVcard);
928             } else {
929                 return mVcardManager.composeAndSendPhonebookOneVcard(op, intIndex, vcard21, null,
930                         mOrderBy, appParamValue.ignorefilter, appParamValue.filter);
931             }
932         } else {
933             if (intIndex <= 0 || intIndex > size) {
934                 Log.w(TAG, "The requested vcard is not acceptable! name= " + name);
935                 return ResponseCodes.OBEX_HTTP_NOT_FOUND;
936             }
937             // For others (ich/och/cch/mch), 0.vcf is meaningless, and must
938             // begin from 1.vcf
939             if (intIndex >= 1) {
940                 return mVcardManager.composeAndSendCallLogVcards(appParamValue.needTag, op,
941                         intIndex, intIndex, vcard21, appParamValue.ignorefilter,
942                         appParamValue.filter);
943             }
944         }
945         return ResponseCodes.OBEX_HTTP_OK;
946     }
947 
pullPhonebook(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, Operation op, final String name)948     private final int pullPhonebook(byte[] appParam, AppParamValue appParamValue, HeaderSet reply,
949             Operation op, final String name) {
950         // code start for passing PTS3.2 TC_PSE_PBD_BI_01_C
951         if (name != null) {
952             int dotIndex = name.indexOf(".");
953             String vcf = "vcf";
954             if (dotIndex >= 0 && dotIndex <= name.length()) {
955                 if (name.regionMatches(dotIndex + 1, vcf, 0, vcf.length()) == false) {
956                     Log.w(TAG, "name is not .vcf");
957                     return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
958                 }
959             }
960         } // code end for passing PTS3.2 TC_PSE_PBD_BI_01_C
961 
962         int pbSize = mVcardManager.getPhonebookSize(appParamValue.needTag);
963         int needSendBody = handleAppParaForResponse(appParamValue, pbSize, reply, op);
964         if (needSendBody != NEED_SEND_BODY) {
965             return needSendBody;
966         }
967 
968         if (pbSize == 0) {
969             if (V) Log.v(TAG, "PhonebookSize is 0, return.");
970             return ResponseCodes.OBEX_HTTP_OK;
971         }
972 
973         int requestSize = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount
974                 : pbSize;
975         int startPoint = appParamValue.listStartOffset;
976         if (startPoint < 0 || startPoint >= pbSize) {
977             Log.w(TAG, "listStartOffset is not correct! " + startPoint);
978             return ResponseCodes.OBEX_HTTP_OK;
979         }
980 
981         // Limit the number of call log to CALLLOG_NUM_LIMIT
982         if (appParamValue.needTag != BluetoothPbapObexServer.ContentType.PHONEBOOK) {
983             if (requestSize > CALLLOG_NUM_LIMIT) {
984                requestSize = CALLLOG_NUM_LIMIT;
985             }
986         }
987 
988         int endPoint = startPoint + requestSize - 1;
989         if (endPoint > pbSize - 1) {
990             endPoint = pbSize - 1;
991         }
992         if (D) Log.d(TAG, "pullPhonebook(): requestSize=" + requestSize + " startPoint=" +
993                 startPoint + " endPoint=" + endPoint);
994 
995         boolean vcard21 = appParamValue.vcard21;
996         if (appParamValue.needTag == BluetoothPbapObexServer.ContentType.PHONEBOOK) {
997             if (startPoint == 0) {
998                 String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21,null);
999                 if (endPoint == 0) {
1000                     return pushBytes(op, ownerVcard);
1001                 } else {
1002                     return mVcardManager.composeAndSendPhonebookVcards(op, 1, endPoint, vcard21,
1003                             ownerVcard, appParamValue.ignorefilter, appParamValue.filter);
1004                 }
1005             } else {
1006                 return mVcardManager.composeAndSendPhonebookVcards(op, startPoint, endPoint,
1007                         vcard21, null, appParamValue.ignorefilter, appParamValue.filter);
1008             }
1009         } else {
1010             return mVcardManager.composeAndSendCallLogVcards(appParamValue.needTag, op,
1011                     startPoint + 1, endPoint + 1, vcard21, appParamValue.ignorefilter,
1012                     appParamValue.filter);
1013         }
1014     }
1015 
closeStream(final OutputStream out, final Operation op)1016     public static boolean closeStream(final OutputStream out, final Operation op) {
1017         boolean returnvalue = true;
1018         try {
1019             if (out != null) {
1020                 out.close();
1021             }
1022         } catch (IOException e) {
1023             Log.e(TAG, "outputStream close failed" + e.toString());
1024             returnvalue = false;
1025         }
1026         try {
1027             if (op != null) {
1028                 op.close();
1029             }
1030         } catch (IOException e) {
1031             Log.e(TAG, "operation close failed" + e.toString());
1032             returnvalue = false;
1033         }
1034         return returnvalue;
1035     }
1036 
1037     // Reserved for future use. In case PSE challenge PCE and PCE input wrong
1038     // session key.
onAuthenticationFailure(final byte[] userName)1039     public final void onAuthenticationFailure(final byte[] userName) {
1040     }
1041 
createSelectionPara(final int type)1042     public static final String createSelectionPara(final int type) {
1043         String selection = null;
1044         switch (type) {
1045             case ContentType.INCOMING_CALL_HISTORY:
1046                 selection = Calls.TYPE + "=" + CallLog.Calls.INCOMING_TYPE;
1047                 break;
1048             case ContentType.OUTGOING_CALL_HISTORY:
1049                 selection = Calls.TYPE + "=" + CallLog.Calls.OUTGOING_TYPE;
1050                 break;
1051             case ContentType.MISSED_CALL_HISTORY:
1052                 selection = Calls.TYPE + "=" + CallLog.Calls.MISSED_TYPE;
1053                 break;
1054             default:
1055                 break;
1056         }
1057         if (V) Log.v(TAG, "Call log selection: " + selection);
1058         return selection;
1059     }
1060 
1061     /**
1062      * XML encode special characters in the name field
1063      */
xmlEncode(String name, StringBuilder result)1064     private void xmlEncode(String name, StringBuilder result) {
1065         if (name == null) {
1066             return;
1067         }
1068 
1069         final StringCharacterIterator iterator = new StringCharacterIterator(name);
1070         char character =  iterator.current();
1071         while (character != CharacterIterator.DONE ){
1072             if (character == '<') {
1073                 result.append("&lt;");
1074             }
1075             else if (character == '>') {
1076                 result.append("&gt;");
1077             }
1078             else if (character == '\"') {
1079                 result.append("&quot;");
1080             }
1081             else if (character == '\'') {
1082                 result.append("&#039;");
1083             }
1084             else if (character == '&') {
1085                 result.append("&amp;");
1086             }
1087             else {
1088                 // The char is not a special one, add it to the result as is
1089                 result.append(character);
1090             }
1091             character = iterator.next();
1092         }
1093     }
1094 
writeVCardEntry(int vcfIndex, String name, StringBuilder result)1095     private void writeVCardEntry(int vcfIndex, String name, StringBuilder result) {
1096         result.append("<card handle=\"");
1097         result.append(vcfIndex);
1098         result.append(".vcf\" name=\"");
1099         xmlEncode(name, result);
1100         result.append("\"/>");
1101     }
1102 
notifyUpdateWakeLock()1103     private void notifyUpdateWakeLock() {
1104         Message msg = Message.obtain(mCallback);
1105         msg.what = BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK;
1106         msg.sendToTarget();
1107     }
1108 
logHeader(HeaderSet hs)1109     public static final void logHeader(HeaderSet hs) {
1110         Log.v(TAG, "Dumping HeaderSet " + hs.toString());
1111         try {
1112 
1113             Log.v(TAG, "COUNT : " + hs.getHeader(HeaderSet.COUNT));
1114             Log.v(TAG, "NAME : " + hs.getHeader(HeaderSet.NAME));
1115             Log.v(TAG, "TYPE : " + hs.getHeader(HeaderSet.TYPE));
1116             Log.v(TAG, "LENGTH : " + hs.getHeader(HeaderSet.LENGTH));
1117             Log.v(TAG, "TIME_ISO_8601 : " + hs.getHeader(HeaderSet.TIME_ISO_8601));
1118             Log.v(TAG, "TIME_4_BYTE : " + hs.getHeader(HeaderSet.TIME_4_BYTE));
1119             Log.v(TAG, "DESCRIPTION : " + hs.getHeader(HeaderSet.DESCRIPTION));
1120             Log.v(TAG, "TARGET : " + hs.getHeader(HeaderSet.TARGET));
1121             Log.v(TAG, "HTTP : " + hs.getHeader(HeaderSet.HTTP));
1122             Log.v(TAG, "WHO : " + hs.getHeader(HeaderSet.WHO));
1123             Log.v(TAG, "OBJECT_CLASS : " + hs.getHeader(HeaderSet.OBJECT_CLASS));
1124             Log.v(TAG, "APPLICATION_PARAMETER : " + hs.getHeader(HeaderSet.APPLICATION_PARAMETER));
1125         } catch (IOException e) {
1126             Log.e(TAG, "dump HeaderSet error " + e);
1127         }
1128     }
1129 }
1130