1 /*
2  * Copyright (C) 2015 Samsung System LSI
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 package com.android.bluetooth.map;
16 
17 import android.bluetooth.BluetoothProfile;
18 import android.bluetooth.BluetoothProtoEnums;
19 import android.util.Log;
20 import android.util.Xml;
21 
22 import com.android.bluetooth.BluetoothStatsLog;
23 import com.android.bluetooth.Utils;
24 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
25 
26 import org.xmlpull.v1.XmlPullParser;
27 import org.xmlpull.v1.XmlPullParserException;
28 import org.xmlpull.v1.XmlSerializer;
29 
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.StringWriter;
33 import java.io.UnsupportedEncodingException;
34 import java.util.HashMap;
35 import java.util.Locale;
36 
37 /** Class to contain a single folder element representation. */
38 // Next tag value for ContentProfileErrorReportUtils.report(): 3
39 public class BluetoothMapFolderElement implements Comparable<BluetoothMapFolderElement> {
40     private String mName;
41     private BluetoothMapFolderElement mParent = null;
42     private long mFolderId = -1;
43     private boolean mHasSmsMmsContent = false;
44     private boolean mHasImContent = false;
45     private boolean mHasEmailContent = false;
46 
47     private boolean mIgnore = false;
48 
49     private HashMap<String, BluetoothMapFolderElement> mSubFolders;
50 
51     private static final String TAG = "BluetoothMapFolderElement";
52 
BluetoothMapFolderElement(String name, BluetoothMapFolderElement parrent)53     public BluetoothMapFolderElement(String name, BluetoothMapFolderElement parrent) {
54         this.mName = name;
55         this.mParent = parrent;
56         mSubFolders = new HashMap<String, BluetoothMapFolderElement>();
57     }
58 
setIgnore(boolean ignore)59     public void setIgnore(boolean ignore) {
60         mIgnore = ignore;
61     }
62 
shouldIgnore()63     public boolean shouldIgnore() {
64         return mIgnore;
65     }
66 
getName()67     public String getName() {
68         return mName;
69     }
70 
hasSmsMmsContent()71     public boolean hasSmsMmsContent() {
72         return mHasSmsMmsContent;
73     }
74 
getFolderId()75     public long getFolderId() {
76         return mFolderId;
77     }
78 
hasEmailContent()79     public boolean hasEmailContent() {
80         return mHasEmailContent;
81     }
82 
setFolderId(long folderId)83     public void setFolderId(long folderId) {
84         this.mFolderId = folderId;
85     }
86 
setHasSmsMmsContent(boolean hasSmsMmsContent)87     public void setHasSmsMmsContent(boolean hasSmsMmsContent) {
88         this.mHasSmsMmsContent = hasSmsMmsContent;
89     }
90 
setHasEmailContent(boolean hasEmailContent)91     public void setHasEmailContent(boolean hasEmailContent) {
92         this.mHasEmailContent = hasEmailContent;
93     }
94 
setHasImContent(boolean hasImContent)95     public void setHasImContent(boolean hasImContent) {
96         this.mHasImContent = hasImContent;
97     }
98 
hasImContent()99     public boolean hasImContent() {
100         return mHasImContent;
101     }
102 
103     /**
104      * Fetch the parent folder.
105      *
106      * @return the parent folder or null if we are at the root folder.
107      */
getParent()108     public BluetoothMapFolderElement getParent() {
109         return mParent;
110     }
111 
112     /**
113      * Build the full path to this folder
114      *
115      * @return a string representing the full path.
116      */
getFullPath()117     public String getFullPath() {
118         StringBuilder sb = new StringBuilder(mName);
119         BluetoothMapFolderElement current = mParent;
120         while (current != null) {
121             if (current.getParent() != null) {
122                 sb.insert(0, current.mName + "/");
123             }
124             current = current.getParent();
125         }
126         // sb.insert(0, "/"); Should this be included? The MAP spec. do not include it in examples.
127         return sb.toString();
128     }
129 
getFolderByName(String name)130     public BluetoothMapFolderElement getFolderByName(String name) {
131         BluetoothMapFolderElement folderElement = this.getRoot();
132         folderElement = folderElement.getSubFolder("telecom");
133         folderElement = folderElement.getSubFolder("msg");
134         folderElement = folderElement.getSubFolder(name);
135         if (folderElement != null && folderElement.getFolderId() == -1) {
136             folderElement = null;
137         }
138         return folderElement;
139     }
140 
getFolderById(long id)141     public BluetoothMapFolderElement getFolderById(long id) {
142         return getFolderById(id, this);
143     }
144 
getFolderById( long id, BluetoothMapFolderElement folderStructure)145     public static BluetoothMapFolderElement getFolderById(
146             long id, BluetoothMapFolderElement folderStructure) {
147         if (folderStructure == null) {
148             return null;
149         }
150         return findFolderById(id, folderStructure.getRoot());
151     }
152 
findFolderById( long id, BluetoothMapFolderElement folder)153     private static BluetoothMapFolderElement findFolderById(
154             long id, BluetoothMapFolderElement folder) {
155         if (folder.getFolderId() == id) {
156             return folder;
157         }
158         /* Else */
159         for (BluetoothMapFolderElement subFolder :
160                 folder.mSubFolders
161                         .values()
162                         .toArray(new BluetoothMapFolderElement[folder.mSubFolders.size()])) {
163             BluetoothMapFolderElement ret = findFolderById(id, subFolder);
164             if (ret != null) {
165                 return ret;
166             }
167         }
168         return null;
169     }
170 
171     /**
172      * Fetch the root folder.
173      *
174      * @return the root folder.
175      */
getRoot()176     public BluetoothMapFolderElement getRoot() {
177         BluetoothMapFolderElement rootFolder = this;
178         while (rootFolder.getParent() != null) {
179             rootFolder = rootFolder.getParent();
180         }
181         return rootFolder;
182     }
183 
184     /**
185      * Add a virtual folder.
186      *
187      * @param name the name of the folder to add.
188      * @return the added folder element.
189      */
addFolder(String name)190     public BluetoothMapFolderElement addFolder(String name) {
191         name = name.toLowerCase(Locale.US);
192         BluetoothMapFolderElement newFolder = mSubFolders.get(name);
193         if (newFolder == null) {
194             Log.d(TAG, "addFolder():" + name);
195             newFolder = new BluetoothMapFolderElement(name, this);
196             mSubFolders.put(name, newFolder);
197         } else {
198             Log.d(TAG, "addFolder():" + name + " already added");
199         }
200         return newFolder;
201     }
202 
203     /**
204      * Add a sms/mms folder.
205      *
206      * @param name the name of the folder to add.
207      * @return the added folder element.
208      */
addSmsMmsFolder(String name)209     public BluetoothMapFolderElement addSmsMmsFolder(String name) {
210         Log.d(TAG, "addSmsMmsFolder()");
211         BluetoothMapFolderElement newFolder = addFolder(name);
212         newFolder.setHasSmsMmsContent(true);
213         return newFolder;
214     }
215 
216     /**
217      * Add a im folder.
218      *
219      * @param name the name of the folder to add.
220      * @return the added folder element.
221      */
addImFolder(String name, long idFolder)222     public BluetoothMapFolderElement addImFolder(String name, long idFolder) {
223         Log.d(TAG, "addImFolder() id = " + idFolder);
224         BluetoothMapFolderElement newFolder = addFolder(name);
225         newFolder.setHasImContent(true);
226         newFolder.setFolderId(idFolder);
227         return newFolder;
228     }
229 
230     /**
231      * Add an Email folder.
232      *
233      * @param name the name of the folder to add.
234      * @return the added folder element.
235      */
addEmailFolder(String name, long emailFolderId)236     public BluetoothMapFolderElement addEmailFolder(String name, long emailFolderId) {
237         Log.v(TAG, "addEmailFolder() id = " + emailFolderId);
238         BluetoothMapFolderElement newFolder = addFolder(name);
239         newFolder.setFolderId(emailFolderId);
240         newFolder.setHasEmailContent(true);
241         return newFolder;
242     }
243 
244     /**
245      * Fetch the number of sub folders.
246      *
247      * @return returns the number of sub folders.
248      */
getSubFolderCount()249     public int getSubFolderCount() {
250         return mSubFolders.size();
251     }
252 
253     /**
254      * Returns the subFolder element matching the supplied folder name.
255      *
256      * @param folderName the name of the subFolder to find.
257      * @return the subFolder element if found {@code null} otherwise.
258      */
getSubFolder(String folderName)259     public BluetoothMapFolderElement getSubFolder(String folderName) {
260         return mSubFolders.get(folderName.toLowerCase());
261     }
262 
encode(int offset, int count)263     public byte[] encode(int offset, int count) throws UnsupportedEncodingException {
264         StringWriter sw = new StringWriter();
265         XmlSerializer xmlMsgElement = Xml.newSerializer();
266         int i, stopIndex;
267         // We need index based access to the subFolders
268         BluetoothMapFolderElement[] folders =
269                 mSubFolders.values().toArray(new BluetoothMapFolderElement[mSubFolders.size()]);
270 
271         if (offset > mSubFolders.size()) {
272             throw new IllegalArgumentException("FolderListingEncode: offset > subFolders.size()");
273         }
274 
275         stopIndex = offset + count;
276         if (stopIndex > mSubFolders.size()) {
277             stopIndex = mSubFolders.size();
278         }
279 
280         try {
281             xmlMsgElement.setOutput(sw);
282             xmlMsgElement.startDocument("UTF-8", true);
283             xmlMsgElement.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
284             xmlMsgElement.startTag(null, "folder-listing");
285             xmlMsgElement.attribute(null, "version", BluetoothMapUtils.MAP_V10_STR);
286             for (i = offset; i < stopIndex; i++) {
287                 xmlMsgElement.startTag(null, "folder");
288                 xmlMsgElement.attribute(null, "name", folders[i].getName());
289                 xmlMsgElement.endTag(null, "folder");
290             }
291             xmlMsgElement.endTag(null, "folder-listing");
292             xmlMsgElement.endDocument();
293         } catch (IllegalArgumentException e) {
294             ContentProfileErrorReportUtils.report(
295                     BluetoothProfile.MAP,
296                     BluetoothProtoEnums.BLUETOOTH_MAP_FOLDER_ELEMENT,
297                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
298                     0);
299             Log.w(TAG, e);
300             throw new IllegalArgumentException("error encoding folderElement");
301         } catch (IllegalStateException e) {
302             ContentProfileErrorReportUtils.report(
303                     BluetoothProfile.MAP,
304                     BluetoothProtoEnums.BLUETOOTH_MAP_FOLDER_ELEMENT,
305                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
306                     1);
307             Log.w(TAG, e);
308             throw new IllegalArgumentException("error encoding folderElement");
309         } catch (IOException e) {
310             ContentProfileErrorReportUtils.report(
311                     BluetoothProfile.MAP,
312                     BluetoothProtoEnums.BLUETOOTH_MAP_FOLDER_ELEMENT,
313                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
314                     2);
315             Log.w(TAG, e);
316             throw new IllegalArgumentException("error encoding folderElement");
317         }
318         return sw.toString().getBytes("UTF-8");
319     }
320 
321     /* The functions below are useful for implementing a MAP client, reusing the object.
322      * Currently they are only used for test purposes.
323      * */
324 
325     /**
326      * Append sub folders from an XML document as specified in the MAP specification. Attributes
327      * will be inherited from parent folder - with regards to message types in the folder.
328      *
329      * @param xmlDocument - InputStream with the document
330      */
appendSubfolders(InputStream xmlDocument)331     public void appendSubfolders(InputStream xmlDocument)
332             throws XmlPullParserException, IOException {
333         try {
334             XmlPullParser parser = Xml.newPullParser();
335             int type;
336             parser.setInput(xmlDocument, "UTF-8");
337 
338             // First find the folder-listing
339             while ((type = parser.next()) != XmlPullParser.END_TAG
340                     && type != XmlPullParser.END_DOCUMENT) {
341                 // Skip until we get a start tag
342                 if (parser.getEventType() != XmlPullParser.START_TAG) {
343                     continue;
344                 }
345                 // Skip until we get a folder-listing tag
346                 String name = parser.getName();
347                 if (!name.equalsIgnoreCase("folder-listing")) {
348                     Log.w(TAG, "Unknown XML tag: " + name);
349                     Utils.skipCurrentTag(parser);
350                 }
351                 readFolders(parser);
352             }
353         } finally {
354             xmlDocument.close();
355         }
356     }
357 
358     /**
359      * Parses folder elements, and add to mSubFolders.
360      *
361      * @param parser the Xml Parser currently pointing to an folder-listing tag.
362      */
readFolders(XmlPullParser parser)363     public void readFolders(XmlPullParser parser) throws XmlPullParserException, IOException {
364         int type;
365         Log.d(TAG, "readFolders(): ");
366         while ((type = parser.next()) != XmlPullParser.END_TAG
367                 && type != XmlPullParser.END_DOCUMENT) {
368             // Skip until we get a start tag
369             if (parser.getEventType() != XmlPullParser.START_TAG) {
370                 continue;
371             }
372             // Skip until we get a folder-listing tag
373             String name = parser.getName();
374             if (!name.trim().equalsIgnoreCase("folder")) {
375                 Log.w(TAG, "Unknown XML tag: " + name);
376                 Utils.skipCurrentTag(parser);
377                 continue;
378             }
379             int count = parser.getAttributeCount();
380             for (int i = 0; i < count; i++) {
381                 if (parser.getAttributeName(i).trim().equalsIgnoreCase("name")) {
382                     // We found a folder, append to sub folders.
383                     BluetoothMapFolderElement element =
384                             addFolder(parser.getAttributeValue(i).trim());
385                     element.setHasEmailContent(mHasEmailContent);
386                     element.setHasImContent(mHasImContent);
387                     element.setHasSmsMmsContent(mHasSmsMmsContent);
388                 } else {
389                     Log.w(TAG, "Unknown XML attribute: " + parser.getAttributeName(i));
390                 }
391             }
392             parser.nextTag();
393         }
394     }
395 
396     /** Recursive compare of all folder names */
397     @Override
compareTo(BluetoothMapFolderElement another)398     public int compareTo(BluetoothMapFolderElement another) {
399         if (another == null) {
400             return 1;
401         }
402         int ret = mName.compareToIgnoreCase(another.mName);
403         // TODO: Do we want to add compare of folder type?
404         if (ret == 0) {
405             ret = mSubFolders.size() - another.mSubFolders.size();
406             if (ret == 0) {
407                 // Compare all sub folder elements (will do nothing if mSubFolders is empty)
408                 for (BluetoothMapFolderElement subfolder : mSubFolders.values()) {
409                     BluetoothMapFolderElement subfolderAnother =
410                             another.mSubFolders.get(subfolder.getName());
411                     if (subfolderAnother == null) {
412                         Log.d(TAG, subfolder.getFullPath() + " not in another");
413                         return 1;
414                     }
415                     ret = subfolder.compareTo(subfolderAnother);
416                     if (ret != 0) {
417                         Log.d(TAG, subfolder.getFullPath() + " filed compareTo()");
418                         return ret;
419                     }
420                 }
421             } else {
422                 Log.d(
423                         TAG,
424                         "mSubFolders.size(): "
425                                 + mSubFolders.size()
426                                 + " another.mSubFolders.size(): "
427                                 + another.mSubFolders.size());
428             }
429         } else {
430             Log.d(TAG, "mName: " + mName + " another.mName: " + another.mName);
431         }
432         return ret;
433     }
434 
435     @Override
toString()436     public String toString() {
437         return mName;
438     }
439 }
440