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