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*/
15package com.android.bluetooth.map;
16
17import java.io.IOException;
18import java.io.UnsupportedEncodingException;
19import java.text.ParseException;
20import java.text.SimpleDateFormat;
21import java.util.ArrayList;
22import java.util.Date;
23import java.util.List;
24
25import org.xmlpull.v1.XmlPullParser;
26import org.xmlpull.v1.XmlPullParserException;
27import org.xmlpull.v1.XmlSerializer;
28
29import android.util.Log;
30
31import com.android.bluetooth.SignedLongLong;
32import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
33import com.android.internal.util.XmlUtils;
34
35public class BluetoothMapConvoListingElement
36    implements Comparable<BluetoothMapConvoListingElement> {
37
38    public static final String XML_TAG_CONVERSATION = "conversation";
39    private static final String XML_ATT_LAST_ACTIVITY = "last_activity";
40    private static final String XML_ATT_NAME = "name";
41    private static final String XML_ATT_ID = "id";
42    private static final String XML_ATT_READ = "readstatus";
43    private static final String XML_ATT_VERSION_COUNTER = "version_counter";
44    private static final String XML_ATT_SUMMARY = "summary";
45    private static final String TAG = "BluetoothMapConvoListingElement";
46    private static final boolean D = BluetoothMapService.DEBUG;
47    private static final boolean V = BluetoothMapService.VERBOSE;
48
49    private SignedLongLong mId = null;
50    private String mName = ""; //title of the conversation #REQUIRED, but allowed empty
51    private long mLastActivity = -1;
52    private boolean mRead = false;
53    private boolean mReportRead = false; // TODO: Is this needed? - false means UNKNOWN
54    private List<BluetoothMapConvoContactElement> mContacts;
55    private long mVersionCounter = -1;
56    private int mCursorIndex = 0;
57    private TYPE mType = null;
58    private String mSummary = null;
59
60 // Used only to keep track of changes to convoListVersionCounter;
61    private String mSmsMmsContacts = null;
62
63    public int getCursorIndex() {
64        return mCursorIndex;
65    }
66
67    public void setCursorIndex(int cursorIndex) {
68        this.mCursorIndex = cursorIndex;
69        if(D) Log.d(TAG, "setCursorIndex: " + cursorIndex);
70    }
71
72    public long getVersionCounter(){
73        return mVersionCounter;
74    }
75
76    public void setVersionCounter(long vcount){
77        if(D) Log.d(TAG, "setVersionCounter: " + vcount);
78        this.mVersionCounter = vcount;
79    }
80
81    public void incrementVersionCounter() {
82        mVersionCounter++;
83    }
84
85    private void setVersionCounter(String vcount){
86        if(D) Log.d(TAG, "setVersionCounter: " + vcount);
87        try {
88            this.mVersionCounter = Long.parseLong(vcount);
89        } catch (NumberFormatException e) {
90            Log.w(TAG, "unable to parse XML versionCounter:" + vcount);
91            mVersionCounter = -1;
92        }
93    }
94
95    public String getName() {
96        return mName;
97    }
98
99    public void setName(String name) {
100        if(D) Log.d(TAG, "setName: " + name);
101        this.mName = name;
102    }
103
104    public TYPE getType() {
105        return mType;
106    }
107
108    public void setType(TYPE type) {
109        this.mType = type;
110    }
111
112    public List<BluetoothMapConvoContactElement> getContacts() {
113        return mContacts;
114    }
115
116    public void setContacts(List<BluetoothMapConvoContactElement> contacts) {
117        this.mContacts = contacts;
118    }
119
120    public void addContact(BluetoothMapConvoContactElement contact){
121        if(mContacts == null)
122            mContacts = new ArrayList<BluetoothMapConvoContactElement>();
123        mContacts.add(contact);
124    }
125
126    public void removeContact(BluetoothMapConvoContactElement contact){
127        mContacts.remove(contact);
128    }
129
130    public void removeContact(int index){
131        mContacts.remove(index);
132    }
133
134
135    public long getLastActivity() {
136        return mLastActivity;
137    }
138
139    public String getLastActivityString() {
140        SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
141        Date date = new Date(mLastActivity);
142        return format.format(date); // Format to YYYYMMDDTHHMMSS local time
143    }
144
145    public void setLastActivity(long last) {
146        if(D) Log.d(TAG, "setLastActivity: " + last);
147        this.mLastActivity = last;
148    }
149
150    public void setLastActivity(String lastActivity)throws ParseException {
151        // TODO: Encode with time-zone if MCE requests it
152        SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
153        Date date = format.parse(lastActivity);
154        this.mLastActivity = date.getTime();
155    }
156
157    public String getRead() {
158        if(mReportRead == false) {
159            return "UNKNOWN";
160        }
161        return (mRead?"READ":"UNREAD");
162    }
163
164    public boolean getReadBool() {
165        return mRead;
166    }
167
168    public void setRead(boolean read, boolean reportRead) {
169        this.mRead = read;
170        if(D) Log.d(TAG, "setRead: " + read);
171        this.mReportRead = reportRead;
172    }
173
174    private void setRead(String value) {
175        if(value.trim().equalsIgnoreCase("yes")) {
176            mRead = true;
177        } else {
178            mRead = false;
179        }
180        mReportRead = true;
181    }
182
183    /**
184     * Set the conversation ID
185     * @param type 0 if the thread ID is valid across all message types in the instance - else
186     * use one of the CONVO_ID_xxx types.
187     * @param threadId the conversation ID
188     */
189    public void setConvoId(long type, long threadId) {
190        this.mId = new SignedLongLong(threadId,type);
191        if(D) Log.d(TAG, "setConvoId: " + threadId + " type:" + type);
192    }
193
194    public String getConvoId(){
195        return mId.toHexString();
196    }
197
198    public long getCpConvoId() {
199        return mId.getLeastSignificantBits();
200    }
201
202    public void setSummary(String summary) {
203        mSummary = summary;
204    }
205
206    public String getFullSummary() {
207        return mSummary;
208    }
209
210    /* Get a valid UTF-8 string of maximum 256 bytes */
211    private String getSummary() {
212        if(mSummary != null) {
213            try {
214                return new String(BluetoothMapUtils.truncateUtf8StringToBytearray(mSummary, 256),
215                        "UTF-8");
216            } catch (UnsupportedEncodingException e) {
217                // This cannot happen on an Android platform - UTF-8 is mandatory
218                Log.e(TAG,"Missing UTF-8 support on platform", e);
219            }
220        }
221        return null;
222    }
223
224    public String getSmsMmsContacts() {
225        return mSmsMmsContacts;
226    }
227
228    public void setSmsMmsContacts(String smsMmsContacts) {
229        mSmsMmsContacts = smsMmsContacts;
230    }
231
232    public int compareTo(BluetoothMapConvoListingElement e) {
233        if (this.mLastActivity < e.mLastActivity) {
234            return 1;
235        } else if (this.mLastActivity > e.mLastActivity) {
236            return -1;
237        } else {
238            return 0;
239        }
240    }
241
242    /* Encode the MapMessageListingElement into the StringBuilder reference.
243     * Here we have taken the choice not to report empty attributes, to reduce the
244     * amount of data to be transfered over BT. */
245    public void encode(XmlSerializer xmlConvoElement)
246            throws IllegalArgumentException, IllegalStateException, IOException
247    {
248
249            // contruct the XML tag for a single conversation in the convolisting
250            xmlConvoElement.startTag(null, XML_TAG_CONVERSATION);
251            xmlConvoElement.attribute(null, XML_ATT_ID, mId.toHexString());
252            if(mName != null) {
253                xmlConvoElement.attribute(null, XML_ATT_NAME,
254                        BluetoothMapUtils.stripInvalidChars(mName));
255            }
256            if(mLastActivity != -1) {
257                xmlConvoElement.attribute(null, XML_ATT_LAST_ACTIVITY,
258                        getLastActivityString());
259            }
260            // Even though this is implied, the value "UNKNOWN" kind of indicated it is required.
261            if(mReportRead == true) {
262                xmlConvoElement.attribute(null, XML_ATT_READ, getRead());
263            }
264            if(mVersionCounter != -1) {
265                xmlConvoElement.attribute(null, XML_ATT_VERSION_COUNTER,
266                        Long.toString(getVersionCounter()));
267            }
268            if(mSummary != null) {
269                xmlConvoElement.attribute(null, XML_ATT_SUMMARY, getSummary());
270            }
271            if(mContacts != null){
272                for(BluetoothMapConvoContactElement contact:mContacts){
273                   contact.encode(xmlConvoElement);
274                }
275            }
276            xmlConvoElement.endTag(null, XML_TAG_CONVERSATION);
277
278    }
279
280    /**
281     * Consumes a conversation tag. It is expected that the parser is beyond the start-tag event,
282     * with the name "conversation".
283     * @param parser
284     * @return
285     * @throws XmlPullParserException
286     * @throws IOException
287     */
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                if(D) Log.i(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                if(D) Log.i(TAG,"Unknown XML tag: " + name);
326                XmlUtils.skipCurrentTag(parser);
327                continue;
328            }
329        }
330        // As we have extracted all attributes, we should expect an end-tag
331        // parser.nextTag(); // consume the end-tag
332        // TODO: Is this needed? - we should already be at end-tag, as this is the top condition
333
334        return newElement;
335    }
336
337    @Override
338    public boolean equals(Object obj) {
339        if (this == obj) {
340            return true;
341        }
342        if (obj == null) {
343            return false;
344        }
345        if (getClass() != obj.getClass()) {
346            return false;
347        }
348        BluetoothMapConvoListingElement other = (BluetoothMapConvoListingElement) obj;
349        if (mContacts == null) {
350            if (other.mContacts != null) {
351                return false;
352            }
353        } else if (!mContacts.equals(other.mContacts)) {
354            return false;
355        }
356        /* As we use equals only for test, we don't compare auto assigned values
357         * if (mId == null) {
358            if (other.mId != null) {
359                return false;
360            }
361        } else if (!mId.equals(other.mId)) {
362            return false;
363        } */
364
365        if (mLastActivity != other.mLastActivity) {
366            return false;
367        }
368        if (mName == null) {
369            if (other.mName != null) {
370                return false;
371            }
372        } else if (!mName.equals(other.mName)) {
373            return false;
374        }
375        if (mRead != other.mRead) {
376            return false;
377        }
378        return true;
379    }
380
381/*    @Override
382    public boolean equals(Object o) {
383
384        return true;
385    };
386    */
387
388}
389
390
391