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