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