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
17package com.android.gallery3d.ingest;
18
19import com.android.gallery3d.R;
20import com.android.gallery3d.ingest.data.ImportTask;
21import com.android.gallery3d.ingest.data.IngestObjectInfo;
22import com.android.gallery3d.ingest.data.MtpClient;
23import com.android.gallery3d.ingest.data.MtpDeviceIndex;
24
25import android.annotation.TargetApi;
26import android.app.NotificationManager;
27import android.app.PendingIntent;
28import android.app.Service;
29import android.content.Context;
30import android.content.Intent;
31import android.media.MediaScannerConnection;
32import android.media.MediaScannerConnection.MediaScannerConnectionClient;
33import android.mtp.MtpDevice;
34import android.mtp.MtpDeviceInfo;
35import android.net.Uri;
36import android.os.Binder;
37import android.os.Build;
38import android.os.IBinder;
39import android.os.SystemClock;
40import android.support.v4.app.NotificationCompat;
41import android.util.SparseBooleanArray;
42import android.widget.Adapter;
43
44import java.util.ArrayList;
45import java.util.Collection;
46import java.util.List;
47
48/**
49 * Service for MTP importing tasks.
50 */
51@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
52public class IngestService extends Service implements ImportTask.Listener,
53    MtpDeviceIndex.ProgressListener, MtpClient.Listener {
54
55  /**
56   * Convenience class to allow easy access to the service instance.
57   */
58  public class LocalBinder extends Binder {
59    IngestService getService() {
60      return IngestService.this;
61    }
62  }
63
64  private static final int PROGRESS_UPDATE_INTERVAL_MS = 180;
65
66  private MtpClient mClient;
67  private final IBinder mBinder = new LocalBinder();
68  private ScannerClient mScannerClient;
69  private MtpDevice mDevice;
70  private String mDevicePrettyName;
71  private MtpDeviceIndex mIndex;
72  private IngestActivity mClientActivity;
73  private boolean mRedeliverImportFinish = false;
74  private int mRedeliverImportFinishCount = 0;
75  private Collection<IngestObjectInfo> mRedeliverObjectsNotImported;
76  private boolean mRedeliverNotifyIndexChanged = false;
77  private boolean mRedeliverIndexFinish = false;
78  private NotificationManager mNotificationManager;
79  private NotificationCompat.Builder mNotificationBuilder;
80  private long mLastProgressIndexTime = 0;
81  private boolean mNeedRelaunchNotification = false;
82
83  @Override
84  public void onCreate() {
85    super.onCreate();
86    mScannerClient = new ScannerClient(this);
87    mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
88    mNotificationBuilder = new NotificationCompat.Builder(this);
89    // TODO(georgescu): Use a better drawable for the notificaton?
90    mNotificationBuilder.setSmallIcon(android.R.drawable.stat_notify_sync)
91        .setContentIntent(PendingIntent.getActivity(this, 0,
92            new Intent(this, IngestActivity.class), 0));
93    mIndex = MtpDeviceIndex.getInstance();
94    mIndex.setProgressListener(this);
95
96    mClient = new MtpClient(getApplicationContext());
97    List<MtpDevice> devices = mClient.getDeviceList();
98    if (!devices.isEmpty()) {
99      setDevice(devices.get(0));
100    }
101    mClient.addListener(this);
102  }
103
104  @Override
105  public void onDestroy() {
106    mClient.close();
107    mIndex.unsetProgressListener(this);
108    super.onDestroy();
109  }
110
111  @Override
112  public IBinder onBind(Intent intent) {
113    return mBinder;
114  }
115
116  private void setDevice(MtpDevice device) {
117    if (mDevice == device) {
118      return;
119    }
120    mRedeliverImportFinish = false;
121    mRedeliverObjectsNotImported = null;
122    mRedeliverNotifyIndexChanged = false;
123    mRedeliverIndexFinish = false;
124    mDevice = device;
125    mIndex.setDevice(mDevice);
126    if (mDevice != null) {
127      MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo();
128      if (deviceInfo == null) {
129        setDevice(null);
130        return;
131      } else {
132        mDevicePrettyName = deviceInfo.getModel();
133        mNotificationBuilder.setContentTitle(mDevicePrettyName);
134        new Thread(mIndex.getIndexRunnable()).start();
135      }
136    } else {
137      mDevicePrettyName = null;
138    }
139    if (mClientActivity != null) {
140      mClientActivity.notifyIndexChanged();
141    } else {
142      mRedeliverNotifyIndexChanged = true;
143    }
144  }
145
146  protected MtpDeviceIndex getIndex() {
147    return mIndex;
148  }
149
150  protected void setClientActivity(IngestActivity activity) {
151    if (mClientActivity == activity) {
152      return;
153    }
154    mClientActivity = activity;
155    if (mClientActivity == null) {
156      if (mNeedRelaunchNotification) {
157        mNotificationBuilder.setProgress(0, 0, false)
158            .setContentText(getResources().getText(R.string.ingest_scanning_done));
159        mNotificationManager.notify(R.id.ingest_notification_scanning,
160            mNotificationBuilder.build());
161      }
162      return;
163    }
164    mNotificationManager.cancel(R.id.ingest_notification_importing);
165    mNotificationManager.cancel(R.id.ingest_notification_scanning);
166    if (mRedeliverImportFinish) {
167      mClientActivity.onImportFinish(mRedeliverObjectsNotImported,
168          mRedeliverImportFinishCount);
169      mRedeliverImportFinish = false;
170      mRedeliverObjectsNotImported = null;
171    }
172    if (mRedeliverNotifyIndexChanged) {
173      mClientActivity.notifyIndexChanged();
174      mRedeliverNotifyIndexChanged = false;
175    }
176    if (mRedeliverIndexFinish) {
177      mClientActivity.onIndexingFinished();
178      mRedeliverIndexFinish = false;
179    }
180    if (mDevice != null) {
181      mNeedRelaunchNotification = true;
182    }
183  }
184
185  protected void importSelectedItems(SparseBooleanArray selected, Adapter adapter) {
186    List<IngestObjectInfo> importHandles = new ArrayList<IngestObjectInfo>();
187    for (int i = 0; i < selected.size(); i++) {
188      if (selected.valueAt(i)) {
189        Object item = adapter.getItem(selected.keyAt(i));
190        if (item instanceof IngestObjectInfo) {
191          importHandles.add(((IngestObjectInfo) item));
192        }
193      }
194    }
195    ImportTask task = new ImportTask(mDevice, importHandles, mDevicePrettyName, this);
196    task.setListener(this);
197    mNotificationBuilder.setProgress(0, 0, true)
198        .setContentText(getResources().getText(R.string.ingest_importing));
199    startForeground(R.id.ingest_notification_importing,
200        mNotificationBuilder.build());
201    new Thread(task).start();
202  }
203
204  @Override
205  public void deviceAdded(MtpDevice device) {
206    if (mDevice == null) {
207      setDevice(device);
208    }
209  }
210
211  @Override
212  public void deviceRemoved(MtpDevice device) {
213    if (device == mDevice) {
214      mNotificationManager.cancel(R.id.ingest_notification_scanning);
215      mNotificationManager.cancel(R.id.ingest_notification_importing);
216      setDevice(null);
217      mNeedRelaunchNotification = false;
218
219    }
220  }
221
222  @Override
223  public void onImportProgress(int visitedCount, int totalCount,
224      String pathIfSuccessful) {
225    if (pathIfSuccessful != null) {
226      mScannerClient.scanPath(pathIfSuccessful);
227    }
228    mNeedRelaunchNotification = false;
229    if (mClientActivity != null) {
230      mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful);
231    }
232    mNotificationBuilder.setProgress(totalCount, visitedCount, false)
233        .setContentText(getResources().getText(R.string.ingest_importing));
234    mNotificationManager.notify(R.id.ingest_notification_importing,
235        mNotificationBuilder.build());
236  }
237
238  @Override
239  public void onImportFinish(Collection<IngestObjectInfo> objectsNotImported,
240      int visitedCount) {
241    stopForeground(true);
242    mNeedRelaunchNotification = true;
243    if (mClientActivity != null) {
244      mClientActivity.onImportFinish(objectsNotImported, visitedCount);
245    } else {
246      mRedeliverImportFinish = true;
247      mRedeliverObjectsNotImported = objectsNotImported;
248      mRedeliverImportFinishCount = visitedCount;
249      mNotificationBuilder.setProgress(0, 0, false)
250          .setContentText(getResources().getText(R.string.ingest_import_complete));
251      mNotificationManager.notify(R.id.ingest_notification_importing,
252          mNotificationBuilder.build());
253    }
254  }
255
256  @Override
257  public void onObjectIndexed(IngestObjectInfo object, int numVisited) {
258    mNeedRelaunchNotification = false;
259    if (mClientActivity != null) {
260      mClientActivity.onObjectIndexed(object, numVisited);
261    } else {
262      // Throttle the updates to one every PROGRESS_UPDATE_INTERVAL_MS milliseconds
263      long currentTime = SystemClock.uptimeMillis();
264      if (currentTime > mLastProgressIndexTime + PROGRESS_UPDATE_INTERVAL_MS) {
265        mLastProgressIndexTime = currentTime;
266        mNotificationBuilder.setProgress(0, numVisited, true)
267            .setContentText(getResources().getText(R.string.ingest_scanning));
268        mNotificationManager.notify(R.id.ingest_notification_scanning,
269            mNotificationBuilder.build());
270      }
271    }
272  }
273
274  @Override
275  public void onSortingStarted() {
276    if (mClientActivity != null) {
277      mClientActivity.onSortingStarted();
278    }
279  }
280
281  @Override
282  public void onIndexingFinished() {
283    mNeedRelaunchNotification = true;
284    if (mClientActivity != null) {
285      mClientActivity.onIndexingFinished();
286    } else {
287      mNotificationBuilder.setProgress(0, 0, false)
288          .setContentText(getResources().getText(R.string.ingest_scanning_done));
289      mNotificationManager.notify(R.id.ingest_notification_scanning,
290          mNotificationBuilder.build());
291      mRedeliverIndexFinish = true;
292    }
293  }
294
295  // Copied from old Gallery3d code
296  private static final class ScannerClient implements MediaScannerConnectionClient {
297    ArrayList<String> mPaths = new ArrayList<String>();
298    MediaScannerConnection mScannerConnection;
299    boolean mConnected;
300    Object mLock = new Object();
301
302    public ScannerClient(Context context) {
303      mScannerConnection = new MediaScannerConnection(context, this);
304    }
305
306    public void scanPath(String path) {
307      synchronized (mLock) {
308        if (mConnected) {
309          mScannerConnection.scanFile(path, null);
310        } else {
311          mPaths.add(path);
312          mScannerConnection.connect();
313        }
314      }
315    }
316
317    @Override
318    public void onMediaScannerConnected() {
319      synchronized (mLock) {
320        mConnected = true;
321        if (!mPaths.isEmpty()) {
322          for (String path : mPaths) {
323            mScannerConnection.scanFile(path, null);
324          }
325          mPaths.clear();
326        }
327      }
328    }
329
330    @Override
331    public void onScanCompleted(String path, Uri uri) {
332    }
333  }
334}
335