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*/
15package com.android.bluetooth.map;
16
17
18import java.io.IOException;
19import java.io.InputStream;
20import java.io.StringWriter;
21import java.io.UnsupportedEncodingException;
22import java.util.Arrays;
23import java.util.HashMap;
24import java.util.Locale;
25
26import org.xmlpull.v1.XmlPullParser;
27import org.xmlpull.v1.XmlPullParserException;
28import org.xmlpull.v1.XmlSerializer;
29
30import android.util.Log;
31import android.util.Xml;
32
33import com.android.internal.util.FastXmlSerializer;
34import com.android.internal.util.XmlUtils;
35
36
37/**
38 * Class to contain a single folder element representation.
39 *
40 */
41public 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
58    public BluetoothMapFolderElement( String name, BluetoothMapFolderElement parrent){
59        this.mName = name;
60        this.mParent = parrent;
61        mSubFolders = new HashMap<String, BluetoothMapFolderElement>();
62    }
63
64    public void setIngore(boolean ignore) {
65        mIgnore = ignore;
66    }
67
68    public boolean shouldIgnore() {
69        return mIgnore;
70    }
71
72    public String getName() {
73        return mName;
74    }
75
76    public boolean hasSmsMmsContent(){
77        return mHasSmsMmsContent;
78    }
79
80    public long getFolderId(){
81        return mFolderId;
82    }
83    public boolean hasEmailContent(){
84        return mHasEmailContent;
85    }
86
87    public void setFolderId(long folderId) {
88        this.mFolderId = folderId;
89    }
90    public void setHasSmsMmsContent(boolean hasSmsMmsContent) {
91        this.mHasSmsMmsContent = hasSmsMmsContent;
92    }
93    public void setHasEmailContent(boolean hasEmailContent) {
94        this.mHasEmailContent = hasEmailContent;
95    }
96    public void setHasImContent(boolean hasImContent) {
97        this.mHasImContent = hasImContent;
98    }
99
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     */
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     */
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
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
140    public BluetoothMapFolderElement getFolderById(long id) {
141        return getFolderById(id, this);
142    }
143
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
249    public BluetoothMapFolderElement getSubFolder(String folderName){
250        return mSubFolders.get(folderName.toLowerCase());
251    }
252
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     */
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     */
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
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