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