• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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 
21 import com.android.bluetooth.BluetoothStatsLog;
22 import com.android.bluetooth.SignedLongLong;
23 import com.android.bluetooth.Utils;
24 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
25 import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
26 
27 import org.xmlpull.v1.XmlPullParser;
28 import org.xmlpull.v1.XmlPullParserException;
29 import org.xmlpull.v1.XmlSerializer;
30 
31 import java.io.IOException;
32 import java.text.ParseException;
33 import java.text.SimpleDateFormat;
34 import java.util.ArrayList;
35 import java.util.Date;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Objects;
39 
40 // Next tag value for ContentProfileErrorReportUtils.report(): 2
41 public class BluetoothMapConvoListingElement
42         implements Comparable<BluetoothMapConvoListingElement> {
43     private static final String TAG = BluetoothMapConvoListingElement.class.getSimpleName();
44 
45     public static final String XML_TAG_CONVERSATION = "conversation";
46     private static final String XML_ATT_LAST_ACTIVITY = "last_activity";
47     private static final String XML_ATT_NAME = "name";
48     private static final String XML_ATT_ID = "id";
49     private static final String XML_ATT_READ = "readstatus";
50     private static final String XML_ATT_VERSION_COUNTER = "version_counter";
51     private static final String XML_ATT_SUMMARY = "summary";
52 
53     private SignedLongLong mId = null;
54     private String mName = ""; // title of the conversation #REQUIRED, but allowed empty
55     private long mLastActivity = -1;
56     private boolean mRead = false;
57     private boolean mReportRead = false; // TODO: Is this needed? - false means UNKNOWN
58     private List<BluetoothMapConvoContactElement> mContacts;
59     private long mVersionCounter = -1;
60     private int mCursorIndex = 0;
61     private TYPE mType = null;
62     private String mSummary = null;
63 
64     // Used only to keep track of changes to convoListVersionCounter;
65     private String mSmsMmsContacts = null;
66 
getCursorIndex()67     public int getCursorIndex() {
68         return mCursorIndex;
69     }
70 
setCursorIndex(int cursorIndex)71     public void setCursorIndex(int cursorIndex) {
72         this.mCursorIndex = cursorIndex;
73         Log.d(TAG, "setCursorIndex: " + cursorIndex);
74     }
75 
getVersionCounter()76     public long getVersionCounter() {
77         return mVersionCounter;
78     }
79 
setVersionCounter(long vcount)80     public void setVersionCounter(long vcount) {
81         Log.d(TAG, "setVersionCounter: " + vcount);
82         this.mVersionCounter = vcount;
83     }
84 
incrementVersionCounter()85     public void incrementVersionCounter() {
86         mVersionCounter++;
87     }
88 
setVersionCounter(String vcount)89     private void setVersionCounter(String vcount) {
90         Log.d(TAG, "setVersionCounter: " + vcount);
91         try {
92             this.mVersionCounter = Long.parseLong(vcount);
93         } catch (NumberFormatException e) {
94             ContentProfileErrorReportUtils.report(
95                     BluetoothProfile.MAP,
96                     BluetoothProtoEnums.BLUETOOTH_MAP_CONVO_LISTING_ELEMENT,
97                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
98                     0);
99             Log.w(TAG, "unable to parse XML versionCounter:" + vcount);
100             mVersionCounter = -1;
101         }
102     }
103 
getName()104     public String getName() {
105         return mName;
106     }
107 
setName(String name)108     public void setName(String name) {
109         Log.d(TAG, "setName: " + name);
110         this.mName = name;
111     }
112 
getType()113     public TYPE getType() {
114         return mType;
115     }
116 
setType(TYPE type)117     public void setType(TYPE type) {
118         this.mType = type;
119     }
120 
getContacts()121     public List<BluetoothMapConvoContactElement> getContacts() {
122         return mContacts;
123     }
124 
setContacts(List<BluetoothMapConvoContactElement> contacts)125     public void setContacts(List<BluetoothMapConvoContactElement> contacts) {
126         this.mContacts = contacts;
127     }
128 
addContact(BluetoothMapConvoContactElement contact)129     public void addContact(BluetoothMapConvoContactElement contact) {
130         if (mContacts == null) {
131             mContacts = new ArrayList<BluetoothMapConvoContactElement>();
132         }
133         mContacts.add(contact);
134     }
135 
removeContact(BluetoothMapConvoContactElement contact)136     public void removeContact(BluetoothMapConvoContactElement contact) {
137         mContacts.remove(contact);
138     }
139 
removeContact(int index)140     public void removeContact(int index) {
141         mContacts.remove(index);
142     }
143 
getLastActivity()144     public long getLastActivity() {
145         return mLastActivity;
146     }
147 
148     @SuppressWarnings("JavaUtilDate") // TODO: b/365629730 -- prefer Instant or LocalDate
getLastActivityString()149     public String getLastActivityString() {
150         SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT);
151         Date date = new Date(mLastActivity);
152         return format.format(date); // Format to YYYYMMDDTHHMMSS local time
153     }
154 
setLastActivity(long last)155     public void setLastActivity(long last) {
156         Log.d(TAG, "setLastActivity: " + last);
157         this.mLastActivity = last;
158     }
159 
160     @SuppressWarnings("JavaUtilDate") // TODO: b/365629730 -- prefer Instant or LocalDate
setLastActivity(String lastActivity)161     public void setLastActivity(String lastActivity) throws ParseException {
162         // TODO: Encode with time-zone if MCE requests it
163         SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT);
164         Date date = format.parse(lastActivity);
165         this.mLastActivity = date.getTime();
166     }
167 
getRead()168     public String getRead() {
169         if (!mReportRead) {
170             return "UNKNOWN";
171         }
172         return (mRead ? "READ" : "UNREAD");
173     }
174 
getReadBool()175     public boolean getReadBool() {
176         return mRead;
177     }
178 
setRead(boolean read, boolean reportRead)179     public void setRead(boolean read, boolean reportRead) {
180         this.mRead = read;
181         Log.d(TAG, "setRead: " + read);
182         this.mReportRead = reportRead;
183     }
184 
setRead(String value)185     private void setRead(String value) {
186         if (value.trim().equalsIgnoreCase("yes")) {
187             mRead = true;
188         } else {
189             mRead = false;
190         }
191         mReportRead = true;
192     }
193 
194     /**
195      * Set the conversation ID
196      *
197      * @param type 0 if the thread ID is valid across all message types in the instance - else use
198      *     one of the CONVO_ID_xxx types.
199      * @param threadId the conversation ID
200      */
setConvoId(long type, long threadId)201     public void setConvoId(long type, long threadId) {
202         this.mId = new SignedLongLong(threadId, type);
203         Log.d(TAG, "setConvoId: " + threadId + " type:" + type);
204     }
205 
getConvoId()206     public String getConvoId() {
207         return mId.toHexString();
208     }
209 
getCpConvoId()210     public long getCpConvoId() {
211         return mId.leastSignificantBits();
212     }
213 
setSummary(String summary)214     public void setSummary(String summary) {
215         mSummary = summary;
216     }
217 
getFullSummary()218     public String getFullSummary() {
219         return mSummary;
220     }
221 
222     /* Get a valid UTF-8 string of maximum 256 bytes */
getSummary()223     private String getSummary() {
224         if (mSummary != null) {
225             return BluetoothMapUtils.truncateUtf8StringToString(mSummary, 256);
226         }
227         return null;
228     }
229 
getSmsMmsContacts()230     public String getSmsMmsContacts() {
231         return mSmsMmsContacts;
232     }
233 
setSmsMmsContacts(String smsMmsContacts)234     public void setSmsMmsContacts(String smsMmsContacts) {
235         mSmsMmsContacts = smsMmsContacts;
236     }
237 
238     @Override
compareTo(BluetoothMapConvoListingElement e)239     public int compareTo(BluetoothMapConvoListingElement e) {
240         if (this.mLastActivity < e.mLastActivity) {
241             return 1;
242         } else if (this.mLastActivity > e.mLastActivity) {
243             return -1;
244         } else {
245             return 0;
246         }
247     }
248 
249     /* Encode the MapMessageListingElement into the StringBuilder reference.
250      * Here we have taken the choice not to report empty attributes, to reduce the
251      * amount of data to be transferred over BT. */
encode(XmlSerializer xmlConvoElement)252     public void encode(XmlSerializer xmlConvoElement)
253             throws IllegalArgumentException, IllegalStateException, IOException {
254 
255         // construct the XML tag for a single conversation in the convolisting
256         xmlConvoElement.startTag(null, XML_TAG_CONVERSATION);
257         xmlConvoElement.attribute(null, XML_ATT_ID, mId.toHexString());
258         if (mName != null) {
259             xmlConvoElement.attribute(
260                     null, XML_ATT_NAME, BluetoothMapUtils.stripInvalidChars(mName));
261         }
262         if (mLastActivity != -1) {
263             xmlConvoElement.attribute(null, XML_ATT_LAST_ACTIVITY, getLastActivityString());
264         }
265         // Even though this is implied, the value "UNKNOWN" kind of indicated it is required.
266         if (mReportRead) {
267             xmlConvoElement.attribute(null, XML_ATT_READ, getRead());
268         }
269         if (mVersionCounter != -1) {
270             xmlConvoElement.attribute(
271                     null, XML_ATT_VERSION_COUNTER, Long.toString(getVersionCounter()));
272         }
273         if (mSummary != null) {
274             xmlConvoElement.attribute(null, XML_ATT_SUMMARY, getSummary());
275         }
276         if (mContacts != null) {
277             for (BluetoothMapConvoContactElement contact : mContacts) {
278                 contact.encode(xmlConvoElement);
279             }
280         }
281         xmlConvoElement.endTag(null, XML_TAG_CONVERSATION);
282     }
283 
284     /**
285      * Consumes a conversation tag. It is expected that the parser is beyond the start-tag event,
286      * with the name "conversation".
287      */
createFromXml(XmlPullParser parser)288     public static BluetoothMapConvoListingElement createFromXml(XmlPullParser parser)
289             throws XmlPullParserException, IOException, ParseException {
290         BluetoothMapConvoListingElement newElement = new BluetoothMapConvoListingElement();
291         int count = parser.getAttributeCount();
292         int type;
293         for (int i = 0; i < count; i++) {
294             String attributeName = parser.getAttributeName(i).trim();
295             String attributeValue = parser.getAttributeValue(i);
296             if (attributeName.equalsIgnoreCase(XML_ATT_ID)) {
297                 newElement.mId = SignedLongLong.fromString(attributeValue);
298             } else if (attributeName.equalsIgnoreCase(XML_ATT_NAME)) {
299                 newElement.mName = attributeValue;
300             } else if (attributeName.equalsIgnoreCase(XML_ATT_LAST_ACTIVITY)) {
301                 newElement.setLastActivity(attributeValue);
302             } else if (attributeName.equalsIgnoreCase(XML_ATT_READ)) {
303                 newElement.setRead(attributeValue);
304             } else if (attributeName.equalsIgnoreCase(XML_ATT_VERSION_COUNTER)) {
305                 newElement.setVersionCounter(attributeValue);
306             } else if (attributeName.equalsIgnoreCase(XML_ATT_SUMMARY)) {
307                 newElement.setSummary(attributeValue);
308             } else {
309                 Log.w(TAG, "Unknown XML attribute: " + parser.getAttributeName(i));
310             }
311         }
312 
313         // Now determine if we get an end-tag, or a new start tag for contacts
314         while ((type = parser.next()) != XmlPullParser.END_TAG
315                 && type != XmlPullParser.END_DOCUMENT) {
316             // Skip until we get a start tag
317             if (parser.getEventType() != XmlPullParser.START_TAG) {
318                 continue;
319             }
320             // Skip until we get a convocontact tag
321             String name = parser.getName().trim();
322             if (name.equalsIgnoreCase(BluetoothMapConvoContactElement.XML_TAG_CONVOCONTACT)) {
323                 newElement.addContact(BluetoothMapConvoContactElement.createFromXml(parser));
324             } else {
325                 Log.w(TAG, "Unknown XML tag: " + name);
326                 Utils.skipCurrentTag(parser);
327             }
328         }
329         // As we have extracted all attributes, we should expect an end-tag
330         // parser.nextTag(); // consume the end-tag
331         // TODO: Is this needed? - we should already be at end-tag, as this is the top condition
332 
333         return newElement;
334     }
335 
336     @Override
equals(Object obj)337     public boolean equals(Object obj) {
338         if (this == obj) {
339             return true;
340         }
341         if (!(obj instanceof BluetoothMapConvoListingElement other)) {
342             return false;
343         }
344 
345         if (!Objects.equals(mContacts, other.mContacts)) {
346             return false;
347         }
348 
349         // Skip comparing auto assigned value `mId`. Equals is only used for test
350 
351         if (mLastActivity != other.mLastActivity) {
352             return false;
353         }
354         if (!Objects.equals(mName, other.mName)) {
355             return false;
356         }
357         if (mRead != other.mRead) {
358             return false;
359         }
360         return true;
361     }
362 
363     @Override
hashCode()364     public int hashCode() {
365         return Objects.hash(mContacts, mLastActivity, mName, mRead);
366     }
367 }
368