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