RootScanner.java revision b63e8c6ccc1425d56f8b9c801f4bddf906d694e5
1package com.android.mtp;
2
3import android.content.ContentResolver;
4import android.content.res.Resources;
5import android.database.sqlite.SQLiteException;
6import android.net.Uri;
7import android.os.Process;
8import android.provider.DocumentsContract;
9import android.util.Log;
10
11import java.io.IOException;
12import java.util.concurrent.ExecutorService;
13import java.util.concurrent.Executors;
14import java.util.concurrent.FutureTask;
15import java.util.concurrent.TimeUnit;
16
17final class RootScanner {
18    /**
19     * Polling interval in milliseconds used for first SHORT_POLLING_TIMES because it is more
20     * likely to add new root just after the device is added.
21     */
22    private final static long SHORT_POLLING_INTERVAL = 2000;
23
24    /**
25     * Polling interval in milliseconds for low priority polling, when changes are not expected.
26     */
27    private final static long LONG_POLLING_INTERVAL = 30 * 1000;
28
29    /**
30     * @see #SHORT_POLLING_INTERVAL
31     */
32    private final static long SHORT_POLLING_TIMES = 10;
33
34    /**
35     * Milliseconds we wait for background thread when pausing.
36     */
37    private final static long AWAIT_TERMINATION_TIMEOUT = 2000;
38
39    final ContentResolver mResolver;
40    final Resources mResources;
41    final MtpManager mManager;
42    final MtpDatabase mDatabase;
43
44    ExecutorService mExecutor;
45    FutureTask<Void> mCurrentTask;
46
47    RootScanner(
48            ContentResolver resolver,
49            Resources resources,
50            MtpManager manager,
51            MtpDatabase database) {
52        mResolver = resolver;
53        mResources = resources;
54        mManager = manager;
55        mDatabase = database;
56    }
57
58    /**
59     * Notifies a change of the roots list via ContentResolver.
60     */
61    void notifyChange() {
62        final Uri uri =
63                DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY);
64        mResolver.notifyChange(uri, null, false);
65    }
66
67    /**
68     * Starts to check new changes right away.
69     * If the background thread has already gone, it restarts another background thread.
70     */
71    synchronized void resume() {
72        if (mExecutor == null) {
73            // Only single thread updates the database.
74            mExecutor = Executors.newSingleThreadExecutor();
75        }
76        if (mCurrentTask != null) {
77            // Cancel previous task.
78            mCurrentTask.cancel(true);
79        }
80        mCurrentTask = new FutureTask<Void>(new UpdateRootsRunnable(), null);
81        mExecutor.submit(mCurrentTask);
82    }
83
84    /**
85     * Stops background thread and wait for its termination.
86     * @throws InterruptedException
87     */
88    synchronized void pause() throws InterruptedException {
89        if (mExecutor == null) {
90            return;
91        }
92        mExecutor.shutdownNow();
93        if (!mExecutor.awaitTermination(AWAIT_TERMINATION_TIMEOUT, TimeUnit.MILLISECONDS)) {
94            Log.e(MtpDocumentsProvider.TAG, "Failed to terminate RootScanner's background thread.");
95        }
96        mExecutor = null;
97    }
98
99    /**
100     * Runnable to scan roots and update the database information.
101     */
102    private final class UpdateRootsRunnable implements Runnable {
103        @Override
104        public void run() {
105            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
106            int pollingCount = 0;
107            while (!Thread.interrupted()) {
108                final int[] deviceIds = mManager.getOpenedDeviceIds();
109                if (deviceIds.length == 0) {
110                    return;
111                }
112                boolean changed = false;
113                mDatabase.getMapper().startAddingDocuments(null /* parentDocumentId */);
114                for (int deviceId : deviceIds) {
115                    try {
116                        final MtpRoot[] roots = mManager.getRoots(deviceId);
117                        if (mDatabase.getMapper().putRootDocuments(deviceId, mResources, roots)) {
118                            changed = true;
119                        }
120                    } catch (IOException | SQLiteException exception) {
121                        // The error may happen on the device. We would like to continue getting
122                        // roots for other devices.
123                        Log.e(MtpDocumentsProvider.TAG, exception.getMessage());
124                    }
125                }
126                if (mDatabase.getMapper().stopAddingDocuments(null /* parentDocumentId */)) {
127                    changed = true;
128                }
129                if (changed) {
130                    notifyChange();
131                }
132                pollingCount++;
133                try {
134                    // Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is
135                    // more likely to add new root just after the device is added.
136                    // TODO: Use short interval only for a device that is just added.
137                    Thread.sleep(pollingCount > SHORT_POLLING_TIMES ?
138                        LONG_POLLING_INTERVAL : SHORT_POLLING_INTERVAL);
139                } catch (InterruptedException exp) {
140                    // The while condition handles the interrupted flag.
141                    continue;
142                }
143            }
144        }
145    }
146}
147