1 /*
2  * Copyright (C) 2013 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 
17 package com.android.documentsui.roots;
18 
19 import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES;
20 
21 import static androidx.core.util.Preconditions.checkNotNull;
22 
23 import static com.android.documentsui.base.SharedMinimal.DEBUG;
24 import static com.android.documentsui.base.SharedMinimal.VERBOSE;
25 
26 import android.content.BroadcastReceiver.PendingResult;
27 import android.content.ContentProviderClient;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.ApplicationInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ProviderInfo;
34 import android.content.pm.ResolveInfo;
35 import android.database.ContentObserver;
36 import android.database.Cursor;
37 import android.net.Uri;
38 import android.os.AsyncTask;
39 import android.os.Bundle;
40 import android.os.FileUtils;
41 import android.os.Handler;
42 import android.os.Looper;
43 import android.os.SystemClock;
44 import android.provider.DocumentsContract;
45 import android.provider.DocumentsContract.Root;
46 import android.util.Log;
47 
48 import androidx.annotation.GuardedBy;
49 import androidx.annotation.Nullable;
50 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
51 
52 import com.android.documentsui.DocumentsApplication;
53 import com.android.documentsui.R;
54 import com.android.documentsui.UserIdManager;
55 import com.android.documentsui.UserPackage;
56 import com.android.documentsui.archives.ArchivesProvider;
57 import com.android.documentsui.base.LookupApplicationName;
58 import com.android.documentsui.base.Providers;
59 import com.android.documentsui.base.RootInfo;
60 import com.android.documentsui.base.State;
61 import com.android.documentsui.base.UserId;
62 
63 import com.google.common.collect.ArrayListMultimap;
64 import com.google.common.collect.Multimap;
65 import com.google.common.util.concurrent.MoreExecutors;
66 
67 import java.util.ArrayList;
68 import java.util.Collection;
69 import java.util.Collections;
70 import java.util.HashMap;
71 import java.util.HashSet;
72 import java.util.List;
73 import java.util.Map;
74 import java.util.Objects;
75 import java.util.concurrent.CountDownLatch;
76 import java.util.concurrent.ExecutorService;
77 import java.util.concurrent.Executors;
78 import java.util.concurrent.Semaphore;
79 import java.util.concurrent.ThreadPoolExecutor;
80 import java.util.concurrent.TimeUnit;
81 import java.util.function.Function;
82 
83 /**
84  * Cache of known storage backends and their roots.
85  */
86 public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
87     private static final String TAG = "ProvidersCache";
88 
89     // Not all providers are equally well written. If a provider returns
90     // empty results we don't cache them...unless they're in this magical list
91     // of beloved providers.
92     private static final List<String> PERMIT_EMPTY_CACHE = new ArrayList<String>() {{
93         // MTP provider commonly returns no roots (if no devices are attached).
94         add(Providers.AUTHORITY_MTP);
95         // ArchivesProvider doesn't support any roots.
96         add(ArchivesProvider.AUTHORITY);
97     }};
98     private static final int FIRST_LOAD_TIMEOUT_MS = 5000;
99 
100     private final Context mContext;
101 
102     @GuardedBy("mRootsChangedObservers")
103     private final Map<UserId, RootsChangedObserver> mRootsChangedObservers = new HashMap<>();
104 
105     @GuardedBy("mRecentsRoots")
106     private final Map<UserId, RootInfo> mRecentsRoots = new HashMap<>();
107 
108     private final Object mLock = new Object();
109     private final CountDownLatch mFirstLoad = new CountDownLatch(1);
110 
111     @GuardedBy("mLock")
112     private boolean mFirstLoadDone;
113     @GuardedBy("mLock")
114     private PendingResult mBootCompletedResult;
115 
116     @GuardedBy("mLock")
117     private Multimap<UserAuthority, RootInfo> mRoots = ArrayListMultimap.create();
118     @GuardedBy("mLock")
119     private HashSet<UserAuthority> mStoppedAuthorities = new HashSet<>();
120     private final Semaphore mMultiProviderUpdateTaskSemaphore = new Semaphore(1);
121 
122     @GuardedBy("mObservedAuthoritiesDetails")
123     private final Map<UserAuthority, PackageDetails> mObservedAuthoritiesDetails = new HashMap<>();
124 
125     private final UserIdManager mUserIdManager;
126 
ProvidersCache(Context context, UserIdManager userIdManager)127     public ProvidersCache(Context context, UserIdManager userIdManager) {
128         mContext = context;
129         mUserIdManager = userIdManager;
130     }
131 
generateRecentsRoot(UserId rootUserId)132     private RootInfo generateRecentsRoot(UserId rootUserId) {
133         return new RootInfo() {{
134             // Special root for recents
135             userId = rootUserId;
136             derivedIcon = R.drawable.ic_root_recent;
137             derivedType = RootInfo.TYPE_RECENTS;
138             flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_SEARCH;
139             queryArgs = QUERY_ARG_MIME_TYPES;
140             title = mContext.getString(R.string.root_recent);
141             availableBytes = -1;
142         }};
143     }
144 
145     private RootInfo createOrGetRecentsRoot(UserId userId) {
146         return createOrGetByUserId(mRecentsRoots, userId, user -> generateRecentsRoot(user));
147     }
148 
149     private RootsChangedObserver createOrGetRootsChangedObserver(UserId userId) {
150         return createOrGetByUserId(mRootsChangedObservers, userId,
151                 user -> new RootsChangedObserver(user));
152     }
153 
154     private static <T> T createOrGetByUserId(Map<UserId, T> map, UserId userId,
155             Function<UserId, T> supplier) {
156         synchronized (map) {
157             if (!map.containsKey(userId)) {
158                 map.put(userId, supplier.apply(userId));
159             }
160         }
161         return map.get(userId);
162     }
163 
164     private class RootsChangedObserver extends ContentObserver {
165 
166         private final UserId mUserId;
167 
168         RootsChangedObserver(UserId userId) {
169             super(new Handler(Looper.getMainLooper()));
170             mUserId = userId;
171         }
172 
173         @Override
174         public void onChange(boolean selfChange, Uri uri) {
175             if (uri == null) {
176                 Log.w(TAG, "Received onChange event for null uri. Skipping.");
177                 return;
178             }
179             if (DEBUG) {
180                 Log.i(TAG, "Updating roots due to change on user " + mUserId + "at " + uri);
181             }
182             updateAuthorityAsync(mUserId, uri.getAuthority());
183         }
184     }
185 
186     @Override
187     public String getApplicationName(UserId userId, String authority) {
188         return mObservedAuthoritiesDetails.get(
189                 new UserAuthority(userId, authority)).applicationName;
190     }
191 
192     @Override
193     public String getPackageName(UserId userId, String authority) {
194         return mObservedAuthoritiesDetails.get(new UserAuthority(userId, authority)).packageName;
195     }
196 
197     public void updateAsync(boolean forceRefreshAll, @Nullable Runnable callback) {
198 
199         // NOTE: This method is called when the UI language changes.
200         // For that reason we update our RecentsRoot to reflect
201         // the current language.
202         final String title = mContext.getString(R.string.root_recent);
203         for (UserId userId : mUserIdManager.getUserIds()) {
204             RootInfo recentRoot = createOrGetRecentsRoot(userId);
205             recentRoot.title = title;
206             // Nothing else about the root should ever change.
207             assert (recentRoot.authority == null);
208             assert (recentRoot.rootId == null);
209             assert (recentRoot.derivedIcon == R.drawable.ic_root_recent);
210             assert (recentRoot.derivedType == RootInfo.TYPE_RECENTS);
211             assert (recentRoot.flags == (Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD));
212             assert (recentRoot.availableBytes == -1);
213         }
214 
215         new MultiProviderUpdateTask(forceRefreshAll, null, callback).executeOnExecutor(
216                 AsyncTask.THREAD_POOL_EXECUTOR);
217     }
218 
219     public void updatePackageAsync(UserId userId, String packageName) {
220         new MultiProviderUpdateTask(
221                 /* forceRefreshAll= */ false,
222                 new UserPackage(userId, packageName),
223                 /* callback= */ null)
224                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
225     }
226 
227     public void updateAuthorityAsync(UserId userId, String authority) {
228         final ProviderInfo info = userId.getPackageManager(mContext).resolveContentProvider(
229                 authority, 0);
230         if (info != null) {
231             updatePackageAsync(userId, info.packageName);
232         }
233     }
234 
235     void setBootCompletedResult(PendingResult result) {
236         synchronized (mLock) {
237             // Quickly check if we've already finished loading, otherwise hang
238             // out until first pass is finished.
239             if (mFirstLoadDone) {
240                 result.finish();
241             } else {
242                 mBootCompletedResult = result;
243             }
244         }
245     }
246 
247     /**
248      * Block until the first {@link MultiProviderUpdateTask} pass has finished.
249      *
250      * @return {@code true} if cached roots is ready to roll, otherwise
251      * {@code false} if we timed out while waiting.
252      */
253     private boolean waitForFirstLoad() {
254         boolean success = false;
255         try {
256             success = mFirstLoad.await(FIRST_LOAD_TIMEOUT_MS, TimeUnit.MILLISECONDS);
257         } catch (InterruptedException e) {
258         }
259         if (!success) {
260             Log.w(TAG, "Timeout waiting for first update");
261         }
262         return success;
263     }
264 
265     /**
266      * Load roots from authorities that are in stopped state. Normal
267      * {@link MultiProviderUpdateTask} passes ignore stopped applications.
268      */
269     private void loadStoppedAuthorities() {
270         synchronized (mLock) {
271             for (UserAuthority userAuthority : mStoppedAuthorities) {
272                 mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true));
273             }
274             mStoppedAuthorities.clear();
275         }
276     }
277 
278     /**
279      * Load roots from a stopped authority. Normal {@link MultiProviderUpdateTask} passes
280      * ignore stopped applications.
281      */
282     private void loadStoppedAuthority(UserAuthority userAuthority) {
283         synchronized (mLock) {
284             if (!mStoppedAuthorities.contains(userAuthority)) {
285                 return;
286             }
287             if (DEBUG) {
288                 Log.d(TAG, "Loading stopped authority " + userAuthority);
289             }
290             mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true));
291             mStoppedAuthorities.remove(userAuthority);
292         }
293     }
294 
295     /**
296      * Bring up requested provider and query for all active roots. Will consult cached
297      * roots if not forceRefresh. Will query when cached roots is empty (which should never happen).
298      */
299     private Collection<RootInfo> loadRootsForAuthority(UserAuthority userAuthority,
300             boolean forceRefresh) {
301         UserId userId = userAuthority.userId;
302         String authority = userAuthority.authority;
303         if (VERBOSE) Log.v(TAG, "Loading roots on user " + userId + " for " + authority);
304 
305         ContentResolver resolver = userId.getContentResolver(mContext);
306         final ArrayList<RootInfo> roots = new ArrayList<>();
307         final PackageManager pm = userId.getPackageManager(mContext);
308         ProviderInfo provider = pm.resolveContentProvider(
309                 authority, PackageManager.GET_META_DATA);
310         if (provider == null) {
311             Log.w(TAG, "Failed to get provider info for " + authority);
312             return roots;
313         }
314         if (!provider.exported) {
315             Log.w(TAG, "Provider is not exported. Failed to load roots for " + authority);
316             return roots;
317         }
318         if (!provider.grantUriPermissions) {
319             Log.w(TAG, "Provider doesn't grantUriPermissions. Failed to load roots for "
320                     + authority);
321             return roots;
322         }
323         if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.readPermission)
324                 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.writePermission)) {
325             Log.w(TAG, "Provider is not protected by MANAGE_DOCUMENTS. Failed to load roots for "
326                     + authority);
327             return roots;
328         }
329 
330         synchronized (mObservedAuthoritiesDetails) {
331             if (!mObservedAuthoritiesDetails.containsKey(userAuthority)) {
332                 CharSequence appName = pm.getApplicationLabel(provider.applicationInfo);
333                 String packageName = provider.applicationInfo.packageName;
334 
335                 mObservedAuthoritiesDetails.put(
336                         userAuthority, new PackageDetails(appName.toString(), packageName));
337 
338                 // Watch for any future updates
339                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
340                 resolver.registerContentObserver(rootsUri, true,
341                         createOrGetRootsChangedObserver(userId));
342             }
343         }
344 
345         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
346         if (!forceRefresh) {
347             // Look for roots data that we might have cached for ourselves in the
348             // long-lived system process.
349             final Bundle systemCache = resolver.getCache(rootsUri);
350             if (systemCache != null) {
351                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
352                 assert (cachedRoots != null);
353                 if (!cachedRoots.isEmpty() || PERMIT_EMPTY_CACHE.contains(authority)) {
354                     if (VERBOSE) Log.v(TAG, "System cache hit for " + authority);
355                     return cachedRoots;
356                 } else {
357                     Log.w(TAG, "Ignoring empty system cache hit for " + authority);
358                 }
359             }
360         }
361 
362         ContentProviderClient client = null;
363         Cursor cursor = null;
364         try {
365             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
366             cursor = client.query(rootsUri, null, null, null, null);
367             while (cursor.moveToNext()) {
368                 final RootInfo root = RootInfo.fromRootsCursor(userId, authority, cursor);
369                 roots.add(root);
370             }
371         } catch (Exception e) {
372             Log.w(TAG, "Failed to load some roots from " + authority, e);
373             // We didn't load every root from the provider. Don't put it to
374             // system cache so that we'll try loading them again next time even
375             // if forceRefresh is false.
376             return roots;
377         } finally {
378             FileUtils.closeQuietly(cursor);
379             FileUtils.closeQuietly(client);
380         }
381 
382         // Cache these freshly parsed roots over in the long-lived system
383         // process, in case our process goes away. The system takes care of
384         // invalidating the cache if the package or Uri changes.
385         final Bundle systemCache = new Bundle();
386         if (roots.isEmpty() && !PERMIT_EMPTY_CACHE.contains(authority)) {
387             Log.i(TAG, "Provider returned no roots. Possibly naughty: " + authority);
388         } else {
389             systemCache.putParcelableArrayList(TAG, roots);
390             resolver.putCache(rootsUri, systemCache);
391         }
392 
393         return roots;
394     }
395 
396     @Override
397     public RootInfo getRootOneshot(UserId userId, String authority, String rootId) {
398         return getRootOneshot(userId, authority, rootId, false);
399     }
400 
401     public RootInfo getRootOneshot(UserId userId, String authority, String rootId,
402             boolean forceRefresh) {
403         synchronized (mLock) {
404             UserAuthority userAuthority = new UserAuthority(userId, authority);
405             RootInfo root = forceRefresh ? null : getRootLocked(userAuthority, rootId);
406             if (root == null) {
407                 mRoots.replaceValues(userAuthority,
408                         loadRootsForAuthority(userAuthority, forceRefresh));
409                 root = getRootLocked(userAuthority, rootId);
410             }
411             return root;
412         }
413     }
414 
415     public RootInfo getRootBlocking(UserId userId, String authority, String rootId) {
416         waitForFirstLoad();
417         loadStoppedAuthorities();
418         synchronized (mLock) {
419             return getRootLocked(new UserAuthority(userId, authority), rootId);
420         }
421     }
422 
423     private RootInfo getRootLocked(UserAuthority userAuthority, String rootId) {
424         for (RootInfo root : mRoots.get(userAuthority)) {
425             if (Objects.equals(root.rootId, rootId)) {
426                 return root;
427             }
428         }
429         return null;
430     }
431 
432     @Override
433     public RootInfo getRecentsRoot(UserId userId) {
434         return createOrGetRecentsRoot(userId);
435     }
436 
437     public boolean isRecentsRoot(RootInfo root) {
438         return mRecentsRoots.containsValue(root);
439     }
440 
441     @Override
442     public Collection<RootInfo> getRootsBlocking() {
443         waitForFirstLoad();
444         loadStoppedAuthorities();
445         synchronized (mLock) {
446             return new HashSet<>(mRoots.values());
447         }
448     }
449 
450     @Override
451     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
452         waitForFirstLoad();
453         loadStoppedAuthorities();
454         synchronized (mLock) {
455             return ProvidersAccess.getMatchingRoots(mRoots.values(), state);
456         }
457     }
458 
459     @Override
460     public Collection<RootInfo> getRootsForAuthorityBlocking(UserId userId, String authority) {
461         waitForFirstLoad();
462         UserAuthority userAuthority = new UserAuthority(userId, authority);
463         loadStoppedAuthority(userAuthority);
464         synchronized (mLock) {
465             final Collection<RootInfo> roots = mRoots.get(userAuthority);
466             return roots != null ? roots : Collections.<RootInfo>emptyList();
467         }
468     }
469 
470     @Override
471     public RootInfo getDefaultRootBlocking(State state) {
472         RootInfo root = ProvidersAccess.getDefaultRoot(getRootsBlocking(), state);
473         return root != null ? root : createOrGetRecentsRoot(UserId.CURRENT_USER);
474     }
475 
476     public void logCache() {
477         StringBuilder output = new StringBuilder();
478 
479         for (UserAuthority userAuthority : mObservedAuthoritiesDetails.keySet()) {
480             List<String> roots = new ArrayList<>();
481             Uri rootsUri = DocumentsContract.buildRootsUri(userAuthority.authority);
482             Bundle systemCache = userAuthority.userId.getContentResolver(mContext).getCache(
483                     rootsUri);
484             if (systemCache != null) {
485                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
486                 for (RootInfo root : cachedRoots) {
487                     roots.add(root.toDebugString());
488                 }
489             }
490 
491             output.append((output.length() == 0) ? "System cache: " : ", ");
492             output.append(userAuthority).append("=").append(roots);
493         }
494 
495         Log.i(TAG, output.toString());
496     }
497 
498     private class MultiProviderUpdateTask extends AsyncTask<Void, Void, Void> {
499         private final boolean mForceRefreshAll;
500         @Nullable
501         private final UserPackage mForceRefreshUserPackage;
502         @Nullable
503         private final Runnable mCallback;
504 
505         @GuardedBy("mLock")
506         private Multimap<UserAuthority, RootInfo> mLocalRoots = ArrayListMultimap.create();
507         @GuardedBy("mLock")
508         private HashSet<UserAuthority> mLocalStoppedAuthorities = new HashSet<>();
509 
510         /**
511          * Create task to update roots cache.
512          *
513          * @param forceRefreshAll when true, all previously cached values for
514          *            all packages should be ignored.
515          * @param forceRefreshUserPackage when non-null, all previously cached
516          *            values for this specific user package should be ignored.
517          * @param callback when non-null, it will be invoked after the task is executed.
518          */
519         MultiProviderUpdateTask(
520                 boolean forceRefreshAll,
521                 @Nullable UserPackage forceRefreshUserPackage,
522                 @Nullable Runnable callback) {
523             mForceRefreshAll = forceRefreshAll;
524             mForceRefreshUserPackage = forceRefreshUserPackage;
525             mCallback = callback;
526         }
527 
528         @Override
529         protected Void doInBackground(Void... params) {
530             if (!mMultiProviderUpdateTaskSemaphore.tryAcquire()) {
531                 // Abort, since previous update task is still running.
532                 return null;
533             }
534 
535             int previousPriority = Thread.currentThread().getPriority();
536             Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
537 
538             final long start = SystemClock.elapsedRealtime();
539 
540             for (UserId userId : mUserIdManager.getUserIds()) {
541                 final RootInfo recents = createOrGetRecentsRoot(userId);
542                 synchronized (mLock) {
543                     mLocalRoots.put(new UserAuthority(recents.userId, recents.authority), recents);
544                 }
545             }
546 
547             List<SingleProviderUpdateTaskInfo> taskInfos = new ArrayList<>();
548             for (UserId userId : mUserIdManager.getUserIds()) {
549                 final PackageManager pm = userId.getPackageManager(mContext);
550                 // Pick up provider with action string
551                 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
552                 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
553                 for (ResolveInfo info : providers) {
554                     ProviderInfo providerInfo = info.providerInfo;
555                     if (providerInfo.authority != null) {
556                         taskInfos.add(new SingleProviderUpdateTaskInfo(providerInfo, userId));
557                     }
558                 }
559             }
560 
561             if (!taskInfos.isEmpty()) {
562                 CountDownLatch updateTaskInternalCountDown = new CountDownLatch(taskInfos.size());
563                 ExecutorService executor = MoreExecutors.getExitingExecutorService(
564                         (ThreadPoolExecutor) Executors.newCachedThreadPool());
565                 for (SingleProviderUpdateTaskInfo taskInfo: taskInfos) {
566                     executor.submit(() ->
567                             startSingleProviderUpdateTask(
568                                     taskInfo.providerInfo,
569                                     taskInfo.userId,
570                                     updateTaskInternalCountDown));
571                 }
572 
573                 // Block until all SingleProviderUpdateTask threads finish executing.
574                 // Use a shorter timeout for first load since it could block picker UI.
575                 long timeoutMs = mFirstLoadDone ? 15000 : FIRST_LOAD_TIMEOUT_MS;
576                 boolean success = false;
577                 try {
578                     success = updateTaskInternalCountDown.await(timeoutMs, TimeUnit.MILLISECONDS);
579                 } catch (InterruptedException e) {
580                 }
581                 if (!success) {
582                     Log.w(TAG, "Timeout executing update task!");
583                 }
584             }
585 
586             final long delta = SystemClock.elapsedRealtime() - start;
587             synchronized (mLock) {
588                 mFirstLoadDone = true;
589                 if (mBootCompletedResult != null) {
590                     mBootCompletedResult.finish();
591                     mBootCompletedResult = null;
592                 }
593                 mRoots = mLocalRoots;
594                 mStoppedAuthorities = mLocalStoppedAuthorities;
595             }
596             if (VERBOSE) {
597                 Log.v(TAG, "Update found " + mLocalRoots.size() + " roots in " + delta + "ms");
598             }
599 
600             mFirstLoad.countDown();
601             LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent(BROADCAST_ACTION));
602             mMultiProviderUpdateTaskSemaphore.release();
603 
604             Thread.currentThread().setPriority(previousPriority);
605             return null;
606         }
607 
608         @Override
609         protected void onPostExecute(Void aVoid) {
610             if (mCallback != null) {
611                 mCallback.run();
612             }
613         }
614 
615         private void startSingleProviderUpdateTask(
616                 ProviderInfo providerInfo,
617                 UserId userId,
618                 CountDownLatch updateCountDown) {
619             int previousPriority = Thread.currentThread().getPriority();
620             Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
621             handleDocumentsProvider(providerInfo, userId);
622             updateCountDown.countDown();
623             Thread.currentThread().setPriority(previousPriority);
624         }
625 
626         private void handleDocumentsProvider(ProviderInfo info, UserId userId) {
627             UserAuthority userAuthority = new UserAuthority(userId, info.authority);
628             // Ignore stopped packages for now; we might query them
629             // later during UI interaction.
630             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
631                 if (VERBOSE) {
632                     Log.v(TAG, "Ignoring stopped authority " + info.authority + ", user " + userId);
633                 }
634                 synchronized (mLock) {
635                     mLocalStoppedAuthorities.add(userAuthority);
636                 }
637                 return;
638             }
639 
640             final boolean forceRefresh = mForceRefreshAll
641                     || Objects.equals(
642                     new UserPackage(userId, info.packageName), mForceRefreshUserPackage);
643             synchronized (mLock) {
644                 mLocalRoots.putAll(userAuthority,
645                         loadRootsForAuthority(userAuthority, forceRefresh));
646             }
647         }
648     }
649 
650     private static class UserAuthority {
651         private final UserId userId;
652         @Nullable
653         private final String authority;
654 
655         private UserAuthority(UserId userId, @Nullable String authority) {
656             this.userId = checkNotNull(userId);
657             this.authority = authority;
658         }
659 
660         @Override
661         public boolean equals(Object o) {
662             if (o == null) {
663                 return false;
664             }
665 
666             if (this == o) {
667                 return true;
668             }
669 
670             if (o instanceof UserAuthority) {
671                 UserAuthority other = (UserAuthority) o;
672                 return Objects.equals(userId, other.userId)
673                         && Objects.equals(authority, other.authority);
674             }
675 
676             return false;
677         }
678 
679 
680         @Override
681         public int hashCode() {
682             return Objects.hash(userId, authority);
683         }
684     }
685 
686     private static class SingleProviderUpdateTaskInfo {
687         private final ProviderInfo providerInfo;
688         private final UserId userId;
689 
690         SingleProviderUpdateTaskInfo(ProviderInfo providerInfo, UserId userId) {
691             this.providerInfo = providerInfo;
692             this.userId = userId;
693         }
694     }
695 
696     private static class PackageDetails {
697         private String applicationName;
698         private String packageName;
699 
700         public PackageDetails(String appName, String pckgName) {
701             applicationName = appName;
702             packageName = pckgName;
703         }
704     }
705 }
706