1 /*
2  * Copyright (C) 2011 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.providers.contacts;
17 
18 import static android.app.AppOpsManager.MODE_ALLOWED;
19 import static android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD;
20 
21 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
22 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.AppOpsManager;
27 import android.content.AttributionSource;
28 import android.content.ContentProvider;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.database.Cursor;
33 import android.net.Uri;
34 import android.os.Binder;
35 import android.os.IBinder;
36 import android.os.ParcelFileDescriptor;
37 import android.provider.BaseColumns;
38 import android.provider.VoicemailContract;
39 import android.provider.VoicemailContract.Status;
40 import android.provider.VoicemailContract.Voicemails;
41 import android.util.ArraySet;
42 import android.util.Log;
43 
44 import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
45 import com.android.providers.contacts.util.ContactsPermissions;
46 import com.android.providers.contacts.util.PackageUtils;
47 import com.android.providers.contacts.util.SelectionBuilder;
48 import com.android.providers.contacts.util.TypedUriMatcherImpl;
49 import com.android.providers.contacts.util.UserUtils;
50 
51 import com.google.common.annotations.VisibleForTesting;
52 
53 import java.io.FileNotFoundException;
54 import java.util.Arrays;
55 import java.util.List;
56 
57 /**
58  * An implementation of the Voicemail content provider. This class in the entry point for both
59  * voicemail content ('calls') table and 'voicemail_status' table. This class performs all common
60  * permission checks and then delegates database level operations to respective table delegate
61  * objects.
62  */
63 public class VoicemailContentProvider extends ContentProvider
64         implements VoicemailTable.DelegateHelper {
65     private static final String TAG = "VoicemailProvider";
66 
67     public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
68 
69     private static final int BACKGROUND_TASK_SCAN_STALE_PACKAGES = 0;
70 
71     private ContactsTaskScheduler mTaskScheduler;
72 
73     private VoicemailPermissions mVoicemailPermissions;
74     private VoicemailTable.Delegate mVoicemailContentTable;
75     private VoicemailTable.Delegate mVoicemailStatusTable;
76 
77     @Override
onCreate()78     public boolean onCreate() {
79         if (VERBOSE_LOGGING) {
80             Log.v(TAG, "onCreate: " + this.getClass().getSimpleName()
81                     + " user=" + android.os.Process.myUserHandle().getIdentifier());
82         }
83         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
84             Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
85         }
86         Context context = context();
87 
88         // Read and write permission requires ADD_VOICEMAIL or carrier privileges. We can't declare
89         // any permission entries in the manifest because carrier-privileged apps without
90         // ADD_VOICEMAIL would be blocked by the platform without even reaching our custom
91         // enforce{Read,Write}PermissionInner functions. These overrides are what allow carrier-
92         // privileged apps to bypass these runtime-configured permissions.
93         // TODO(b/74245334): See if these can be removed since individual operations perform their
94         // own checks.
95         setReadPermission(android.Manifest.permission.ADD_VOICEMAIL);
96         setWritePermission(android.Manifest.permission.ADD_VOICEMAIL);
97         setAppOps(AppOpsManager.OP_ADD_VOICEMAIL, AppOpsManager.OP_ADD_VOICEMAIL);
98 
99         mVoicemailPermissions = new VoicemailPermissions(context);
100         mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context,
101                 getDatabaseHelper(context), this, createCallLogInsertionHelper(context));
102         mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context,
103                 getDatabaseHelper(context), this);
104 
105         mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
106             @Override
107             public void onPerformTask(int taskId, Object arg) {
108                 performBackgroundTask(taskId, arg);
109             }
110         };
111 
112         scheduleScanStalePackages();
113 
114         ContactsPackageMonitor.start(getContext());
115 
116         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
117             Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish");
118         }
119         return true;
120     }
121 
122     @Override
enforceReadPermissionInner(Uri uri, @NonNull AttributionSource attributionSource)123     protected int enforceReadPermissionInner(Uri uri,
124             @NonNull AttributionSource attributionSource) throws SecurityException {
125         // Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state.
126         if (mVoicemailPermissions.callerHasCarrierPrivileges()) {
127             return MODE_ALLOWED;
128         }
129         return super.enforceReadPermissionInner(uri, attributionSource);
130     }
131 
132     @Override
enforceWritePermissionInner(Uri uri, @NonNull AttributionSource attributionSource)133     protected int enforceWritePermissionInner(Uri uri,
134             @NonNull AttributionSource attributionSource) throws SecurityException {
135         // Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state.
136         if (mVoicemailPermissions.callerHasCarrierPrivileges()) {
137             return MODE_ALLOWED;
138         }
139         return super.enforceWritePermissionInner(uri, attributionSource);
140     }
141 
142     @VisibleForTesting
scheduleScanStalePackages()143     void scheduleScanStalePackages() {
144         scheduleTask(BACKGROUND_TASK_SCAN_STALE_PACKAGES, null);
145     }
146 
147     @VisibleForTesting
scheduleTask(int taskId, Object arg)148     void scheduleTask(int taskId, Object arg) {
149         mTaskScheduler.scheduleTask(taskId, arg);
150     }
151 
152     @VisibleForTesting
createCallLogInsertionHelper(Context context)153     /*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
154         return DefaultCallLogInsertionHelper.getInstance(context);
155     }
156 
157     @VisibleForTesting
getDatabaseHelper(Context context)158     /*package*/ CallLogDatabaseHelper getDatabaseHelper(Context context) {
159         return CallLogDatabaseHelper.getInstance(context);
160     }
161 
162     @VisibleForTesting
context()163     /*package*/ Context context() {
164         return getContext();
165     }
166 
167     @Override
getType(Uri uri)168     public String getType(Uri uri) {
169         UriData uriData = null;
170         try {
171             uriData = UriData.createUriData(uri);
172         } catch (IllegalArgumentException ignored) {
173             // Special case: for illegal URIs, we return null rather than thrown an exception.
174             return null;
175         }
176         return getTableDelegate(uriData).getType(uriData);
177     }
178 
179     @Override
insert(Uri uri, ContentValues values)180     public Uri insert(Uri uri, ContentValues values) {
181         if (VERBOSE_LOGGING) {
182             Log.v(TAG, "insert: uri=" + uri + "  values=[" + values + "]" +
183                     " CPID=" + Binder.getCallingPid() +
184                     " CUID=" + Binder.getCallingUid());
185         }
186         UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
187         return getTableDelegate(uriData).insert(uriData, values);
188     }
189 
190     @Override
bulkInsert(@onNull Uri uri, @NonNull ContentValues[] values)191     public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
192         UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
193         return getTableDelegate(uriData).bulkInsert(uriData, values);
194     }
195 
196     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)197     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
198             String sortOrder) {
199         if (VERBOSE_LOGGING) {
200             Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
201                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
202                     "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
203                     " CUID=" + Binder.getCallingUid() +
204                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
205         }
206         UriData uriData = checkPermissionsAndCreateUriDataForRead(uri);
207         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
208         selectionBuilder.addClause(getPackageRestrictionClause(true/*isQuery*/));
209         return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
210                 selectionArgs, sortOrder);
211     }
212 
213     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)214     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
215         if (VERBOSE_LOGGING) {
216             Log.v(TAG, "update: uri=" + uri +
217                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
218                     "  values=[" + values + "] CPID=" + Binder.getCallingPid() +
219                     " CUID=" + Binder.getCallingUid() +
220                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
221         }
222         UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
223         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
224         selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/));
225         return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(),
226                 selectionArgs);
227     }
228 
229     @Override
delete(Uri uri, String selection, String[] selectionArgs)230     public int delete(Uri uri, String selection, String[] selectionArgs) {
231         if (VERBOSE_LOGGING) {
232             Log.v(TAG, "delete: uri=" + uri +
233                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
234                     " CPID=" + Binder.getCallingPid() +
235                     " CUID=" + Binder.getCallingUid() +
236                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
237         }
238         UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri);
239         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
240         selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/));
241         return getTableDelegate(uriData).delete(uriData, selectionBuilder.build(), selectionArgs);
242     }
243 
244     @Override
openFile(Uri uri, String mode)245     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
246         boolean success = false;
247         try {
248             UriData uriData = null;
249             if (mode.equals("r")) {
250                 uriData = checkPermissionsAndCreateUriDataForRead(uri);
251             } else {
252                 uriData = checkPermissionsAndCreateUriDataForWrite(uri);
253             }
254             // openFileHelper() relies on "_data" column to be populated with the file path.
255             final ParcelFileDescriptor ret = getTableDelegate(uriData).openFile(uriData, mode);
256             success = true;
257             return ret;
258         } finally {
259             if (VERBOSE_LOGGING) {
260                 Log.v(TAG, "openFile uri=" + uri + " mode=" + mode + " success=" + success +
261                         " CPID=" + Binder.getCallingPid() +
262                         " CUID=" + Binder.getCallingUid() +
263                         " User=" + UserUtils.getCurrentUserHandle(getContext()));
264             }
265         }
266     }
267 
268     /** Returns the correct table delegate object that can handle this URI. */
getTableDelegate(UriData uriData)269     private VoicemailTable.Delegate getTableDelegate(UriData uriData) {
270         switch (uriData.getUriType()) {
271             case STATUS:
272             case STATUS_ID:
273                 return mVoicemailStatusTable;
274             case VOICEMAILS:
275             case VOICEMAILS_ID:
276                 return mVoicemailContentTable;
277             case NO_MATCH:
278                 throw new IllegalStateException("Invalid uri type for uri: " + uriData.getUri());
279             default:
280                 throw new IllegalStateException("Impossible, all cases are covered.");
281         }
282     }
283 
284     /**
285      * Decorates a URI by providing methods to get various properties from the URI.
286      */
287     public static class UriData {
288         private final Uri mUri;
289         private final String mId;
290         private final String mSourcePackage;
291         private final VoicemailUriType mUriType;
292 
UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage)293         private UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
294             mUriType = uriType;
295             mUri = uri;
296             mId = id;
297             mSourcePackage = sourcePackage;
298         }
299 
300         /** Gets the original URI to which this {@link UriData} corresponds. */
getUri()301         public final Uri getUri() {
302             return mUri;
303         }
304 
305         /** Tells us if our URI has an individual voicemail id. */
hasId()306         public final boolean hasId() {
307             return mId != null;
308         }
309 
310         /** Gets the ID for the voicemail. */
getId()311         public final String getId() {
312             return mId;
313         }
314 
315         /** Tells us if our URI has a source package string. */
hasSourcePackage()316         public final boolean hasSourcePackage() {
317             return mSourcePackage != null;
318         }
319 
320         /** Gets the source package. */
getSourcePackage()321         public final String getSourcePackage() {
322             return mSourcePackage;
323         }
324 
325         /** Gets the Voicemail URI type. */
getUriType()326         public final VoicemailUriType getUriType() {
327             return mUriType;
328         }
329 
330         /** Builds a where clause from the URI data. */
getWhereClause()331         public final String getWhereClause() {
332             return concatenateClauses(
333                     (hasId() ? getEqualityClause(BaseColumns._ID, getId()) : null),
334                     (hasSourcePackage() ? getEqualityClause(SOURCE_PACKAGE_FIELD,
335                             getSourcePackage()) : null));
336         }
337 
338         /** Create a {@link UriData} corresponding to a given uri. */
createUriData(Uri uri)339         public static UriData createUriData(Uri uri) {
340             String sourcePackage = uri.getQueryParameter(
341                     VoicemailContract.PARAM_KEY_SOURCE_PACKAGE);
342             List<String> segments = uri.getPathSegments();
343             VoicemailUriType uriType = createUriMatcher().match(uri);
344             switch (uriType) {
345                 case VOICEMAILS:
346                 case STATUS:
347                     return new UriData(uri, uriType, null, sourcePackage);
348                 case VOICEMAILS_ID:
349                 case STATUS_ID:
350                     return new UriData(uri, uriType, segments.get(1), sourcePackage);
351                 case NO_MATCH:
352                     throw new IllegalArgumentException("Invalid URI: " + uri);
353                 default:
354                     throw new IllegalStateException("Impossible, all cases are covered");
355             }
356         }
357 
createUriMatcher()358         private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
359             return new TypedUriMatcherImpl<VoicemailUriType>(
360                     VoicemailContract.AUTHORITY, VoicemailUriType.values());
361         }
362     }
363 
364     @Override
365     // VoicemailTable.DelegateHelper interface.
checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values)366     public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) {
367         // If content values don't contain the provider, calculate the right provider to use.
368         if (!values.containsKey(SOURCE_PACKAGE_FIELD)) {
369             String provider = uriData.hasSourcePackage() ?
370                     uriData.getSourcePackage() : getInjectedCallingPackage();
371             values.put(SOURCE_PACKAGE_FIELD, provider);
372         }
373 
374         // You must have access to the provider given in values.
375         if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
376             checkPackagesMatch(getInjectedCallingPackage(),
377                     values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD),
378                     uriData.getUri());
379         }
380     }
381 
382     /**
383      * Checks that the source_package field is same in uriData and ContentValues, if it happens
384      * to be set in both.
385      */
checkSourcePackageSameIfSet(UriData uriData, ContentValues values)386     private void checkSourcePackageSameIfSet(UriData uriData, ContentValues values) {
387         if (uriData.hasSourcePackage() && values.containsKey(SOURCE_PACKAGE_FIELD)) {
388             if (!uriData.getSourcePackage().equals(values.get(SOURCE_PACKAGE_FIELD))) {
389                 throw new SecurityException(
390                         "source_package in URI was " + uriData.getSourcePackage() +
391                         " but doesn't match source_package in ContentValues which was "
392                         + values.get(SOURCE_PACKAGE_FIELD));
393             }
394         }
395     }
396 
397     @Override
398     /** Implementation of  {@link VoicemailTable.DelegateHelper#openDataFile(UriData, String)} */
openDataFile(UriData uriData, String mode)399     public ParcelFileDescriptor openDataFile(UriData uriData, String mode)
400             throws FileNotFoundException {
401         return openFileHelper(uriData.getUri(), mode);
402     }
403 
404     /**
405      * Ensures that the caller has the permissions to perform a query/read operation, and
406      * then returns the structured representation {@link UriData} of the supplied uri.
407      */
checkPermissionsAndCreateUriDataForRead(Uri uri)408     private UriData checkPermissionsAndCreateUriDataForRead(Uri uri) {
409         // If the caller has been explicitly granted read permission to this URI then no need to
410         // check further.
411         if (ContactsPermissions.hasCallerUriPermission(
412                 getContext(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)) {
413             return UriData.createUriData(uri);
414         }
415 
416         if (mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) {
417             return UriData.createUriData(uri);
418         }
419 
420         return checkPermissionsAndCreateUriData(uri, true);
421     }
422 
423     /**
424      * Performs necessary voicemail permission checks common to all operations and returns
425      * the structured representation, {@link UriData}, of the supplied uri.
426      */
checkPermissionsAndCreateUriData(Uri uri, boolean read)427     private UriData checkPermissionsAndCreateUriData(Uri uri, boolean read) {
428         UriData uriData = UriData.createUriData(uri);
429         if (!hasReadWritePermission(read)) {
430             mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
431             checkPackagePermission(uriData);
432         }
433         return uriData;
434     }
435 
436     /**
437      * Ensures that the caller has the permissions to perform an update/delete operation, and
438      * then returns the structured representation {@link UriData} of the supplied uri.
439      * Also does a permission check on the ContentValues.
440      */
checkPermissionsAndCreateUriDataForWrite(Uri uri, ContentValues... valuesArray)441     private UriData checkPermissionsAndCreateUriDataForWrite(Uri uri, ContentValues... valuesArray) {
442         UriData uriData = checkPermissionsAndCreateUriData(uri, false);
443         for (ContentValues values : valuesArray) {
444             checkSourcePackageSameIfSet(uriData, values);
445         }
446         return uriData;
447     }
448 
449     /**
450      * Checks that the callingPackage is same as voicemailSourcePackage. Throws {@link
451      * SecurityException} if they don't match.
452      */
checkPackagesMatch(String callingPackage, String voicemailSourcePackage, Uri uri)453     private final void checkPackagesMatch(String callingPackage, String voicemailSourcePackage,
454             Uri uri) {
455         if (!voicemailSourcePackage.equals(callingPackage)) {
456             String errorMsg = String.format("Permission denied for URI: %s\n. " +
457                     "Package %s cannot perform this operation for %s. Requires %s permission.",
458                     uri, callingPackage, voicemailSourcePackage,
459                     android.Manifest.permission.WRITE_VOICEMAIL);
460             throw new SecurityException(errorMsg);
461         }
462     }
463 
464     /**
465      * Checks that either the caller has the MANAGE_VOICEMAIL permission,
466      * or has the ADD_VOICEMAIL permission and is using a URI that matches
467      * /voicemail/?source_package=[source-package] where [source-package] is the same as the calling
468      * package.
469      *
470      * @throws SecurityException if the check fails.
471      */
checkPackagePermission(UriData uriData)472     private void checkPackagePermission(UriData uriData) {
473         if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
474             if (!uriData.hasSourcePackage()) {
475                 // You cannot have a match if this is not a provider URI.
476                 throw new SecurityException(String.format(
477                         "Provider %s does not have %s permission." +
478                                 "\nPlease set query parameter '%s' in the URI.\nURI: %s",
479                         getInjectedCallingPackage(), android.Manifest.permission.WRITE_VOICEMAIL,
480                         VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri()));
481             }
482             checkPackagesMatch(getInjectedCallingPackage(), uriData.getSourcePackage(),
483                     uriData.getUri());
484         }
485     }
486 
487     @VisibleForTesting
getInjectedCallingPackage()488     String getInjectedCallingPackage() {
489         return super.getCallingPackage();
490     }
491 
492     /**
493      * Creates a clause to restrict the selection to the calling provider or null if the caller has
494      * access to all data.
495      */
getPackageRestrictionClause(boolean isQuery)496     private String getPackageRestrictionClause(boolean isQuery) {
497         if (hasReadWritePermission(isQuery)) {
498             return null;
499         }
500         return getEqualityClause(Voicemails.SOURCE_PACKAGE, getInjectedCallingPackage());
501     }
502 
503     /**
504      * Whether or not the calling package has the appropriate read/write permission. The user
505      * selected default and/or system dialers are always allowed to read and write to the
506      * VoicemailContentProvider.
507      *
508      * @param read Whether or not this operation is a read
509      *
510      * @return True if the package has the permission required to perform the read/write operation
511      */
hasReadWritePermission(boolean read)512     private boolean hasReadWritePermission(boolean read) {
513         return read ? mVoicemailPermissions.callerHasReadAccess(getCallingPackage()) :
514             mVoicemailPermissions.callerHasWriteAccess(getCallingPackage());
515     }
516 
517     /** Remove all records from a given source package. */
removeBySourcePackage(String packageName)518     public void removeBySourcePackage(String packageName) {
519         delete(Voicemails.buildSourceUri(packageName), null, null);
520         delete(Status.buildSourceUri(packageName), null, null);
521     }
522 
523     @VisibleForTesting
performBackgroundTask(int task, Object arg)524     void performBackgroundTask(int task, Object arg) {
525         switch (task) {
526             case BACKGROUND_TASK_SCAN_STALE_PACKAGES:
527                 removeStalePackages();
528                 break;
529         }
530     }
531 
532     /**
533      * Remove all records made by packages that no longer exist.
534      */
removeStalePackages()535     private void removeStalePackages() {
536         if (VERBOSE_LOGGING) {
537             Log.v(TAG, "scanStalePackages start");
538         }
539 
540         // Make sure all source tables still exists.
541 
542         // First, list all source packages.
543         final ArraySet<String> packages = mVoicemailContentTable.getSourcePackages();
544         packages.addAll(mVoicemailStatusTable.getSourcePackages());
545 
546         // Remove the ones that still exist.
547         for (int i = packages.size() - 1; i >= 0; i--) {
548             final String pkg = packages.valueAt(i);
549             final boolean installed = PackageUtils.isPackageInstalled(getContext(), pkg);
550             if (VERBOSE_LOGGING) {
551                 Log.v(TAG, "  " + pkg + (installed ? " installed" : " removed"));
552             }
553             if (!installed) {
554                 removeBySourcePackage(pkg);
555             }
556         }
557 
558         if (VERBOSE_LOGGING) {
559             Log.v(TAG, "scanStalePackages finish");
560         }
561     }
562 }
563