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