1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 package com.android.voicemail.impl.sync;
17 
18 import android.content.ContentResolver;
19 import android.content.ContentUris;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.net.Uri;
24 import android.provider.VoicemailContract;
25 import android.provider.VoicemailContract.Voicemails;
26 import android.support.annotation.NonNull;
27 import android.telecom.PhoneAccountHandle;
28 import com.android.dialer.common.Assert;
29 import com.android.voicemail.impl.Voicemail;
30 import java.util.ArrayList;
31 import java.util.List;
32 
33 /** Construct queries to interact with the voicemails table. */
34 public class VoicemailsQueryHelper {
35   static final String[] PROJECTION =
36       new String[] {
37         Voicemails._ID, // 0
38         Voicemails.SOURCE_DATA, // 1
39         Voicemails.IS_READ, // 2
40         Voicemails.DELETED, // 3
41         Voicemails.TRANSCRIPTION // 4
42       };
43 
44   public static final int _ID = 0;
45   public static final int SOURCE_DATA = 1;
46   public static final int IS_READ = 2;
47   public static final int DELETED = 3;
48   public static final int TRANSCRIPTION = 4;
49 
50   static final String DELETED_SELECTION = Voicemails.DELETED + "=1";
51   static final String ARCHIVED_SELECTION = Voicemails.ARCHIVED + "=0";
52 
53   private Context context;
54   private ContentResolver contentResolver;
55   private Uri sourceUri;
56 
VoicemailsQueryHelper(Context context)57   public VoicemailsQueryHelper(Context context) {
58     this.context = context;
59     contentResolver = context.getContentResolver();
60     sourceUri = VoicemailContract.Voicemails.buildSourceUri(this.context.getPackageName());
61   }
62 
63   /**
64    * Get all the locally deleted voicemails that have not been synced to the server.
65    *
66    * @return A list of deleted voicemails.
67    */
getDeletedVoicemails(@onNull PhoneAccountHandle phoneAccountHandle)68   public List<Voicemail> getDeletedVoicemails(@NonNull PhoneAccountHandle phoneAccountHandle) {
69     return getLocalVoicemails(phoneAccountHandle, DELETED_SELECTION);
70   }
71 
72   /**
73    * Get all voicemails locally stored.
74    *
75    * @return A list of all locally stored voicemails.
76    */
getAllVoicemails(@onNull PhoneAccountHandle phoneAccountHandle)77   public List<Voicemail> getAllVoicemails(@NonNull PhoneAccountHandle phoneAccountHandle) {
78     return getLocalVoicemails(phoneAccountHandle, null);
79   }
80 
81   /**
82    * Utility method to make queries to the voicemail database.
83    *
84    * <p>TODO(a bug) add PhoneAccountHandle filtering back
85    *
86    * @param selection A filter declaring which rows to return. {@code null} returns all rows.
87    * @return A list of voicemails according to the selection statement.
88    */
getLocalVoicemails( @onNull PhoneAccountHandle unusedPhoneAccountHandle, String selection)89   private List<Voicemail> getLocalVoicemails(
90       @NonNull PhoneAccountHandle unusedPhoneAccountHandle, String selection) {
91     Cursor cursor = contentResolver.query(sourceUri, PROJECTION, selection, null, null);
92     if (cursor == null) {
93       return null;
94     }
95     try {
96       List<Voicemail> voicemails = new ArrayList<Voicemail>();
97       while (cursor.moveToNext()) {
98         final long id = cursor.getLong(_ID);
99         final String sourceData = cursor.getString(SOURCE_DATA);
100         final boolean isRead = cursor.getInt(IS_READ) == 1;
101         final String transcription = cursor.getString(TRANSCRIPTION);
102         Voicemail voicemail =
103             Voicemail.createForUpdate(id, sourceData)
104                 .setIsRead(isRead)
105                 .setTranscription(transcription)
106                 .build();
107         voicemails.add(voicemail);
108       }
109       return voicemails;
110     } finally {
111       cursor.close();
112     }
113   }
114 
115   /**
116    * Deletes a list of voicemails from the voicemail content provider.
117    *
118    * @param voicemails The list of voicemails to delete
119    * @return The number of voicemails deleted
120    */
deleteFromDatabase(List<Voicemail> voicemails)121   public int deleteFromDatabase(List<Voicemail> voicemails) {
122     int count = voicemails.size();
123     if (count == 0) {
124       return 0;
125     }
126 
127     StringBuilder sb = new StringBuilder();
128     for (int i = 0; i < count; i++) {
129       if (i > 0) {
130         sb.append(",");
131       }
132       sb.append(voicemails.get(i).getId());
133     }
134 
135     String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
136     return contentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
137   }
138 
139   /** Utility method to delete a single voicemail that is not archived. */
deleteNonArchivedFromDatabase(Voicemail voicemail)140   public void deleteNonArchivedFromDatabase(Voicemail voicemail) {
141     contentResolver.delete(
142         Voicemails.CONTENT_URI,
143         Voicemails._ID + "=? AND " + Voicemails.ARCHIVED + "= 0",
144         new String[] {Long.toString(voicemail.getId())});
145   }
146 
markReadInDatabase(List<Voicemail> voicemails)147   public int markReadInDatabase(List<Voicemail> voicemails) {
148     int count = voicemails.size();
149     for (int i = 0; i < count; i++) {
150       markReadInDatabase(voicemails.get(i));
151     }
152     return count;
153   }
154 
155   /** Utility method to mark single message as read. */
markReadInDatabase(Voicemail voicemail)156   public void markReadInDatabase(Voicemail voicemail) {
157     Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
158     ContentValues contentValues = new ContentValues();
159     contentValues.put(Voicemails.IS_READ, "1");
160     contentResolver.update(uri, contentValues, null, null);
161   }
162 
163   /**
164    * Sends an update command to the voicemail content provider for a list of voicemails. From the
165    * view of the provider, since the updater is the owner of the entry, a blank "update" means that
166    * the voicemail source is indicating that the server has up-to-date information on the voicemail.
167    * This flips the "dirty" bit to "0".
168    *
169    * @param voicemails The list of voicemails to update
170    * @return The number of voicemails updated
171    */
markCleanInDatabase(List<Voicemail> voicemails)172   public int markCleanInDatabase(List<Voicemail> voicemails) {
173     int count = voicemails.size();
174     for (int i = 0; i < count; i++) {
175       markCleanInDatabase(voicemails.get(i));
176     }
177     return count;
178   }
179 
180   /** Utility method to mark single message as clean. */
markCleanInDatabase(Voicemail voicemail)181   public void markCleanInDatabase(Voicemail voicemail) {
182     Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
183     ContentValues contentValues = new ContentValues();
184     contentResolver.update(uri, contentValues, null, null);
185   }
186 
187   /** Utility method to add a transcription to the voicemail. */
updateWithTranscription(Voicemail voicemail, String transcription)188   public void updateWithTranscription(Voicemail voicemail, String transcription) {
189     Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
190     ContentValues contentValues = new ContentValues();
191     contentValues.put(Voicemails.TRANSCRIPTION, transcription);
192     contentResolver.update(uri, contentValues, null, null);
193   }
194 
195   /**
196    * Voicemail is unique if the tuple of (phone account component name, phone account id, source
197    * data) is unique. If the phone account is missing, we also consider this unique since it's
198    * simply an "unknown" account.
199    *
200    * @param voicemail The voicemail to check if it is unique.
201    * @return {@code true} if the voicemail is unique, {@code false} otherwise.
202    */
isVoicemailUnique(Voicemail voicemail)203   public boolean isVoicemailUnique(Voicemail voicemail) {
204     Cursor cursor = null;
205     PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
206     if (phoneAccount != null) {
207       String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
208       String phoneAccountId = phoneAccount.getId();
209       String sourceData = voicemail.getSourceData();
210       if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
211         return true;
212       }
213       try {
214         String whereClause =
215             Voicemails.PHONE_ACCOUNT_COMPONENT_NAME
216                 + "=? AND "
217                 + Voicemails.PHONE_ACCOUNT_ID
218                 + "=? AND "
219                 + Voicemails.SOURCE_DATA
220                 + "=?";
221         String[] whereArgs = {phoneAccountComponentName, phoneAccountId, sourceData};
222         cursor = contentResolver.query(sourceUri, PROJECTION, whereClause, whereArgs, null);
223         if (cursor.getCount() == 0) {
224           return true;
225         } else {
226           return false;
227         }
228       } finally {
229         if (cursor != null) {
230           cursor.close();
231         }
232       }
233     }
234     return true;
235   }
236 
237   /**
238    * Marks voicemails in the local database as archived. This indicates that the voicemails from the
239    * server were removed automatically to make space for new voicemails, and are stored locally on
240    * the users devices, without a corresponding server copy.
241    */
markArchivedInDatabase(List<Voicemail> voicemails)242   public void markArchivedInDatabase(List<Voicemail> voicemails) {
243     for (Voicemail voicemail : voicemails) {
244       markArchiveInDatabase(voicemail);
245     }
246   }
247 
248   /** Utility method to mark single voicemail as archived. */
markArchiveInDatabase(Voicemail voicemail)249   public void markArchiveInDatabase(Voicemail voicemail) {
250     Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
251     ContentValues contentValues = new ContentValues();
252     contentValues.put(Voicemails.ARCHIVED, "1");
253     contentResolver.update(uri, contentValues, null, null);
254   }
255 
256   /** Find the oldest voicemails that are on the device, and also on the server. */
oldestVoicemailsOnServer(int numVoicemails)257   public List<Voicemail> oldestVoicemailsOnServer(int numVoicemails) {
258     if (numVoicemails <= 0) {
259       Assert.fail("Query for remote voicemails cannot be <= 0");
260     }
261 
262     String sortAndLimit = "date ASC limit " + numVoicemails;
263 
264     try (Cursor cursor =
265         contentResolver.query(sourceUri, PROJECTION, ARCHIVED_SELECTION, null, sortAndLimit)) {
266 
267       Assert.isNotNull(cursor);
268 
269       List<Voicemail> voicemails = new ArrayList<>();
270       while (cursor.moveToNext()) {
271         final long id = cursor.getLong(_ID);
272         final String sourceData = cursor.getString(SOURCE_DATA);
273         Voicemail voicemail = Voicemail.createForUpdate(id, sourceData).build();
274         voicemails.add(voicemail);
275       }
276 
277       if (voicemails.size() != numVoicemails) {
278         Assert.fail(
279             String.format(
280                 "voicemail count (%d) doesn't matched expected (%d)",
281                 voicemails.size(), numVoicemails));
282       }
283       return voicemails;
284     }
285   }
286 }
287