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