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.gallery3d.ingest;
18 
19 import com.android.gallery3d.R;
20 import com.android.gallery3d.ingest.data.ImportTask;
21 import com.android.gallery3d.ingest.data.IngestObjectInfo;
22 import com.android.gallery3d.ingest.data.MtpClient;
23 import com.android.gallery3d.ingest.data.MtpDeviceIndex;
24 
25 import android.annotation.TargetApi;
26 import android.app.NotificationManager;
27 import android.app.PendingIntent;
28 import android.app.Service;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.media.MediaScannerConnection;
32 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
33 import android.mtp.MtpDevice;
34 import android.mtp.MtpDeviceInfo;
35 import android.net.Uri;
36 import android.os.Binder;
37 import android.os.Build;
38 import android.os.IBinder;
39 import android.os.SystemClock;
40 import androidx.core.app.NotificationCompat;
41 import android.util.SparseBooleanArray;
42 import android.widget.Adapter;
43 
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.List;
47 
48 /**
49  * Service for MTP importing tasks.
50  */
51 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
52 public 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 {
getService()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
onCreate()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
onDestroy()105   public void onDestroy() {
106     mClient.close();
107     mIndex.unsetProgressListener(this);
108     super.onDestroy();
109   }
110 
111   @Override
onBind(Intent intent)112   public IBinder onBind(Intent intent) {
113     return mBinder;
114   }
115 
setDevice(MtpDevice device)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 
getIndex()146   protected MtpDeviceIndex getIndex() {
147     return mIndex;
148   }
149 
setClientActivity(IngestActivity activity)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 
importSelectedItems(SparseBooleanArray selected, Adapter adapter)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
deviceAdded(MtpDevice device)205   public void deviceAdded(MtpDevice device) {
206     if (mDevice == null) {
207       setDevice(device);
208     }
209   }
210 
211   @Override
deviceRemoved(MtpDevice device)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
onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful)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
onImportFinish(Collection<IngestObjectInfo> objectsNotImported, int visitedCount)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
onObjectIndexed(IngestObjectInfo object, int numVisited)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
onSortingStarted()275   public void onSortingStarted() {
276     if (mClientActivity != null) {
277       mClientActivity.onSortingStarted();
278     }
279   }
280 
281   @Override
onIndexingFinished()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 
ScannerClient(Context context)302     public ScannerClient(Context context) {
303       mScannerConnection = new MediaScannerConnection(context, this);
304     }
305 
scanPath(String path)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
onMediaScannerConnected()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
onScanCompleted(String path, Uri uri)331     public void onScanCompleted(String path, Uri uri) {
332     }
333   }
334 }
335