github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/clients/android/src/org/camlistore/UploadService.java (about)

     1  /*
     2  Copyright 2011 Google Inc.
     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 org.camlistore;
    18  
    19  import java.io.File;
    20  import java.io.FileNotFoundException;
    21  import java.io.IOException;
    22  import java.util.ArrayList;
    23  import java.util.HashMap;
    24  import java.util.LinkedList;
    25  import java.util.List;
    26  import java.util.Map;
    27  import java.util.Map.Entry;
    28  import java.util.TreeMap;
    29  
    30  import org.camlistore.UploadThread.CamputChunkUploadedMessage;
    31  import org.camlistore.UploadThread.CamputStatsMessage;
    32  
    33  import android.app.Notification;
    34  import android.app.NotificationManager;
    35  import android.app.PendingIntent;
    36  import android.app.Service;
    37  import android.content.ContentResolver;
    38  import android.content.Context;
    39  import android.content.Intent;
    40  import android.database.Cursor;
    41  import android.net.Uri;
    42  import android.net.wifi.WifiManager;
    43  import android.os.Bundle;
    44  import android.os.Environment;
    45  import android.os.FileObserver;
    46  import android.os.IBinder;
    47  import android.os.ParcelFileDescriptor;
    48  import android.os.Parcelable;
    49  import android.os.PowerManager;
    50  import android.os.RemoteException;
    51  import android.provider.MediaStore;
    52  import android.util.Log;
    53  
    54  public class UploadService extends Service {
    55      private static final String TAG = "UploadService";
    56  
    57      private static int NOTIFY_ID_UPLOADING = 0x001;
    58  
    59      public static final String INTENT_POWER_CONNECTED = "POWER_CONNECTED";
    60      public static final String INTENT_POWER_DISCONNECTED = "POWER_DISCONNECTED";
    61      public static final String INTENT_UPLOAD_ALL = "UPLOAD_ALL";
    62      public static final String INTENT_NETWORK_WIFI = "WIFI_NOW";
    63      public static final String INTENT_NETWORK_NOT_WIFI = "NOT_WIFI_NOW";
    64  
    65      // Everything in this block guarded by 'this':
    66      private boolean mUploading = false; // user's desired state (notified
    67                                          // quickly)
    68      private UploadThread mUploadThread = null; // last thread created; null when
    69                                                 // thread exits
    70      private final Map<QueuedFile, Long> mFileBytesRemain = new HashMap<QueuedFile, Long>();
    71      private final LinkedList<QueuedFile> mQueueList = new LinkedList<QueuedFile>();
    72      private final Map<String, Long> mStatValue = new TreeMap<String, Long>();
    73      private IStatusCallback mCallback = DummyNullCallback.instance();
    74      private String mLastUploadStatusText = null; // single line
    75      private String mLastUploadStatsText = null; // multi-line stats
    76      private int mBytesInFlight = 0;
    77      private int mFilesInFlight = 0;
    78  
    79      // Stats, all guarded by 'this', and all reset to 0 on queue size transition
    80      // from 0 -> 1.
    81      private long mBytesTotal = 0;
    82      private long mBytesUploaded = 0;
    83      private int mFilesTotal = 0;
    84      private int mFilesUploaded = 0;
    85  
    86      // Effectively final, initialized in onCreate():
    87      PowerManager mPowerManager;
    88      WifiManager mWifiManager;
    89      NotificationManager mNotificationManager;
    90      Preferences mPrefs;
    91  
    92      // File Observers. Need to keep a reference to them, as there's no JNI
    93      // reference and their finalizers would run otherwise, stopping their
    94      // inotify.
    95      private final ArrayList<FileObserver> mObservers = new ArrayList<FileObserver>();
    96  
    97      @Override
    98      public void onCreate() {
    99          super.onCreate();
   100          Log.d(TAG, "onCreate");
   101  
   102          mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
   103          mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
   104          mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
   105          mPrefs = new Preferences(getSharedPreferences(Preferences.NAME, 0));
   106  
   107          updateBackgroundWatchers();
   108      }
   109  
   110      @Override
   111      public IBinder onBind(Intent intent) {
   112          Log.d(TAG, "onBind intent=" + intent);
   113          return service;
   114      }
   115  
   116      @Override
   117      public void onStart(Intent intent, int startId) {
   118          handleCommand(intent);
   119      }
   120  
   121      private void startUploadService() {
   122          startService(new Intent(UploadService.this, UploadService.class));
   123      }
   124  
   125      // This is @Override as of SDK version 5, but we're targetting 4 (Android
   126      // 1.6)
   127      private static final int START_STICKY = 1; // in SDK 5
   128  
   129      @Override
   130      public int onStartCommand(Intent intent, int flags, int startId) {
   131          handleCommand(intent);
   132          // We want this service to continue running until it is explicitly
   133          // stopped, so return sticky.
   134          return START_STICKY;
   135      }
   136  
   137      private void handleCommand(Intent intent) {
   138          Log.d(TAG, "in handleCommand() for onStart() intent: " + intent);
   139          if (intent == null) {
   140              stopServiceIfEmpty();
   141              return;
   142          }
   143  
   144          String action = intent.getAction();
   145          if (Intent.ACTION_SEND.equals(action)) {
   146              handleSend(intent);
   147              stopServiceIfEmpty();
   148              return;
   149          }
   150  
   151          if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
   152              handleSendMultiple(intent);
   153              stopServiceIfEmpty();
   154              return;
   155          }
   156  
   157          if (INTENT_UPLOAD_ALL.equals(action)) {
   158              handleUploadAll();
   159              return;
   160          }
   161  
   162          try {
   163              if (mPrefs.autoUpload()) {
   164                  boolean startAuto = false;
   165                  boolean stopAuto = false;
   166  
   167                  if (INTENT_POWER_CONNECTED.equals(action)) {
   168                      if (!mPrefs.autoRequiresWifi() || WifiPowerReceiver.onWifi(this)) {
   169                          startAuto = true;
   170                      }
   171                  } else if (INTENT_NETWORK_WIFI.equals(action)) {
   172                      if (!mPrefs.autoRequiresPower() || WifiPowerReceiver.onPower(this)) {
   173                          startAuto = true;
   174                      }
   175                  } else if (INTENT_POWER_DISCONNECTED.equals(action)) {
   176                      stopAuto = mPrefs.autoRequiresPower();
   177                  } else if (INTENT_NETWORK_NOT_WIFI.equals(action)) {
   178                      stopAuto = mPrefs.autoRequiresWifi();
   179                  }
   180  
   181                  if (startAuto) {
   182                      Log.d(TAG, "Starting automatic uploads");
   183                      service.resume();
   184                      handleUploadAll();
   185                      return;
   186                  }
   187                  if (stopAuto) {
   188                      Log.d(TAG, "Stopping automatic uploads");
   189                      service.pause();
   190                      stopBackgroundWatchers();
   191                      stopServiceIfEmpty();
   192                      return;
   193                  }
   194              }
   195          } catch (RemoteException e) {
   196              // Ignore.
   197          }
   198      }
   199  
   200      private void handleSend(Intent intent) {
   201          Bundle extras = intent.getExtras();
   202          if (extras == null) {
   203              Log.w(TAG, "expected extras in handleSend");
   204              return;
   205          }
   206  
   207          extras.keySet(); // unparcel
   208          Log.d(TAG, "handleSend; extras=" + extras);
   209  
   210          Object streamValue = extras.get("android.intent.extra.STREAM");
   211          if (!(streamValue instanceof Uri)) {
   212              Log.w(TAG, "Expected URI for STREAM; got: " + streamValue);
   213              return;
   214          }
   215  
   216          final Uri uri = (Uri) streamValue;
   217          Util.runAsync(new Runnable() {
   218              @Override
   219              public void run() {
   220                  try {
   221                      service.enqueueUpload(uri);
   222                  } catch (RemoteException e) {
   223                  } finally {
   224                      stopServiceIfEmpty();
   225                  }
   226              }
   227          });
   228      }
   229  
   230      private void handleUploadAll() {
   231          startService(new Intent(UploadService.this, UploadService.class));
   232          final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Camli Upload All");
   233          wakeLock.acquire();
   234          Util.runAsync(new Runnable() {
   235              @Override
   236              public void run() {
   237                  try {
   238                      List<String> dirs = getBackupDirs();
   239                      List<Uri> filesToQueue = new ArrayList<Uri>();
   240                      for (String dirName : dirs) {
   241                          File dir = new File(dirName);
   242                          if (!dir.exists()) {
   243                              continue;
   244                          }
   245                          File[] files = dir.listFiles();
   246                          if (files != null) {
   247                              for (int i = 0; i < files.length; ++i) {
   248                                  File f = files[i];
   249                                  if (f.isDirectory()) {
   250                                      // Skip thumbnails directory.
   251                                      // TODO: are any interesting enough to recurse into?
   252                                      // Definitely don't need to upload thumbnails, but
   253                                      // but maybe some other app in the the future creates
   254                                      // sharded directories. Eye-Fi doesn't, though.
   255                                      continue;
   256                                  }
   257                                  filesToQueue.add(Uri.fromFile(f));
   258                              }
   259                          }
   260                      }
   261                      try {
   262                          service.enqueueUploadList(filesToQueue);
   263                      } catch (RemoteException e) {
   264                      } finally {
   265                          stopServiceIfEmpty();
   266                      }
   267                  } finally {
   268                      wakeLock.release();
   269                  }
   270              }
   271          });
   272      }
   273  
   274      private List<String> getBackupDirs() {
   275          ArrayList<String> dirs = new ArrayList<String>();
   276          if (mPrefs.autoDirPhotos()) {
   277              dirs.add(Environment.getExternalStorageDirectory() + "/DCIM/Camera");
   278              dirs.add(Environment.getExternalStorageDirectory() + "/Eye-Fi");
   279          }
   280          if (mPrefs.autoDirMyTracks()) {
   281              dirs.add(Environment.getExternalStorageDirectory() + "/gpx");
   282              dirs.add(Environment.getExternalStorageDirectory() + "/kml");
   283          }
   284          return dirs;
   285      }
   286  
   287      private void handleSendMultiple(Intent intent) {
   288          ArrayList<Parcelable> items = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
   289          ArrayList<Uri> uris = new ArrayList<Uri>(items.size());
   290          for (Parcelable p : items) {
   291              if (!(p instanceof Uri)) {
   292                  Log.d(TAG, "uh, unknown thing " + p);
   293                  continue;
   294              }
   295              uris.add((Uri) p);
   296          }
   297          final ArrayList<Uri> finalUris = uris;
   298          Util.runAsync(new Runnable() {
   299              @Override
   300              public void run() {
   301                  try {
   302                      service.enqueueUploadList(finalUris);
   303                  } catch (RemoteException e) {
   304                  } finally {
   305                      stopServiceIfEmpty();
   306                  }
   307              }
   308          });
   309      }
   310  
   311      private void stopBackgroundWatchers() {
   312          synchronized (UploadService.this) {
   313              if (mObservers.isEmpty()) {
   314                  return;
   315              }
   316              Log.d(TAG, "Stopping background watchers...");
   317              for (FileObserver fo : mObservers) {
   318                  fo.stopWatching();
   319              }
   320              mObservers.clear();
   321          }
   322      }
   323  
   324      private void updateBackgroundWatchers() {
   325          stopBackgroundWatchers();
   326          if (!mPrefs.autoUpload()) {
   327              return;
   328          }
   329          startBackgroundWatchers();
   330      }
   331  
   332      private void startBackgroundWatchers() {
   333          Log.d(TAG, "Starting background watchers...");
   334          synchronized (UploadService.this) {
   335              maybeAddObserver("DCIM/Camera");
   336              maybeAddObserver("Eye-Fi");
   337              maybeAddObserver("gpx");
   338          }
   339      }
   340  
   341      // Requires that UploadService.this is locked.
   342      private void maybeAddObserver(String suffix) {
   343          File f = new File(Environment.getExternalStorageDirectory(), suffix);
   344          if (f.exists()) {
   345              mObservers.add(new CamliFileObserver(service, f));
   346          }
   347      }
   348  
   349      @Override
   350      public void onDestroy() {
   351          synchronized (this) {
   352              Log.d(TAG, "onDestroy of camli UploadService; thread=" + mUploadThread + "; uploading=" + mUploading + "; queue size=" + mFileBytesRemain.size());
   353          }
   354          super.onDestroy();
   355          if (mUploadThread != null) {
   356              Log.e(TAG, "Unexpected onDestroy with active upload thread.  Killing it.");
   357              mUploadThread.interrupt();
   358              mUploadThread = null;
   359          }
   360      }
   361  
   362      // Called by UploadThread to get stuff to do. Caller owns the returned new
   363      // LinkedList. Doesn't return null.
   364      LinkedList<QueuedFile> uploadQueue() {
   365          synchronized (this) {
   366              LinkedList<QueuedFile> copy = new LinkedList<QueuedFile>();
   367              copy.addAll(mQueueList);
   368              return copy;
   369          }
   370      }
   371  
   372      void setUploadStatusText(String status) {
   373          IStatusCallback cb;
   374          synchronized (this) {
   375              mLastUploadStatusText = status;
   376              cb = mCallback;
   377          }
   378          try {
   379              cb.setUploadStatusText(status);
   380          } catch (RemoteException e) {
   381          }
   382      }
   383  
   384      void setInFlightBytes(int v) {
   385          synchronized (this) {
   386              mBytesInFlight = v;
   387          }
   388          broadcastByteStatus();
   389      }
   390  
   391      void broadcastByteStatus() {
   392          synchronized (this) {
   393              try {
   394                  mCallback.setByteStatus(mBytesUploaded, mBytesInFlight, mBytesTotal);
   395              } catch (RemoteException e) {
   396              }
   397          }
   398      }
   399  
   400      void broadcastFileStatus() {
   401          synchronized (this) {
   402              try {
   403                  mCallback.setFileStatus(mFilesUploaded, mFilesInFlight, mFilesTotal);
   404              } catch (RemoteException e) {
   405              }
   406          }
   407      }
   408  
   409      void broadcastAllState() {
   410          synchronized (this) {
   411              try {
   412                  mCallback.setUploading(mUploading);
   413                  mCallback.setUploadStatusText(mLastUploadStatusText);
   414                  mCallback.setUploadStatsText(mLastUploadStatsText);
   415              } catch (RemoteException e) {
   416              }
   417          }
   418          broadcastFileStatus();
   419          broadcastByteStatus();
   420      }
   421  
   422      private void onUploadThreadEnded() {
   423          synchronized (this) {
   424              Log.d(TAG, "UploadThread ended");
   425              mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
   426              mUploadThread = null;
   427              mUploading = false;
   428              try {
   429                  mCallback.setUploading(false);
   430              } catch (RemoteException e) {
   431              }
   432          }
   433          stopServiceIfEmpty();
   434      }
   435  
   436      /**
   437       * Callback from the UploadThread to the service.
   438       * 
   439       * @param qf
   440       *            the queued file that was successfully uploaded.
   441       */
   442      void onUploadComplete(QueuedFile qf) {
   443          Log.d(TAG, "onUploadComplete of " + qf);
   444          synchronized (this) {
   445              if (!mFileBytesRemain.containsKey(qf)) {
   446                  Log.w(TAG, "onUploadComplete of un-queued file: " + qf);
   447                  return;
   448              }
   449              incrBytes(qf, qf.getSize());
   450              mFileBytesRemain.remove(qf);
   451              if (mFileBytesRemain.isEmpty()) {
   452                  // Fill up the percentage bars, since we could get
   453                  // this event before the periodic stats event.
   454                  // And at the end, we could kill camput between
   455                  // getting the final "file uploaded" event and the final
   456                  // stats event.
   457                  mFilesUploaded = mFilesTotal;
   458                  mBytesUploaded = mBytesTotal;
   459                  mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
   460                  stopUploadThread();
   461              }
   462              mQueueList.remove(qf); // TODO: ghetto, linear scan
   463          }
   464          broadcastAllState();
   465          stopServiceIfEmpty();
   466      }
   467  
   468      // incrBytes notes that size bytes of qf have been uploaded
   469      // and updates mBytesUploaded.
   470      private void incrBytes(QueuedFile qf, long size) {
   471          synchronized (this) {
   472              Long remain = mFileBytesRemain.get(qf);
   473              if (remain != null) {
   474                  long actual = Math.min(size, remain.longValue());
   475                  mBytesUploaded += actual;
   476                  mFileBytesRemain.put(qf, remain - actual);
   477              }
   478          }
   479      }
   480  
   481      private void stopServiceIfEmpty() {
   482          // Convenient place to drop this cache.
   483          synchronized (this) {
   484              if (mFileBytesRemain.isEmpty() && !mUploading && mUploadThread == null && !mPrefs.autoUpload()) {
   485                  Log.d(TAG, "stopServiceIfEmpty; stopping");
   486                  stopSelf();
   487              } else {
   488                  Log.d(TAG, "stopServiceIfEmpty; NOT stopping; " + mFileBytesRemain.isEmpty() + "; " + mUploading + "; " + (mUploadThread != null));
   489                  return;
   490              }
   491          }
   492      }
   493  
   494      ParcelFileDescriptor getFileDescriptor(Uri uri) {
   495          ContentResolver cr = getContentResolver();
   496          try {
   497              return cr.openFileDescriptor(uri, "r");
   498          } catch (FileNotFoundException e) {
   499              Log.w(TAG, "FileNotFound in getFileDescriptor() for " + uri);
   500              return null;
   501          }
   502      }
   503  
   504      private void incrementFilesToUpload(int size) throws RemoteException {
   505          synchronized (UploadService.this) {
   506              mFilesTotal += size;
   507          }
   508          broadcastFileStatus();
   509      }
   510  
   511      // pathOfURI tries to return the on-disk absolute path of uri.
   512      // It may return null if it fails.
   513      public String pathOfURI(Uri uri) {
   514          if (uri == null) {
   515              return null;
   516          }
   517          if ("file".equals(uri.getScheme())) {
   518              return uri.getPath();
   519          }
   520          String[] proj = { MediaStore.Images.Media.DATA };
   521          Cursor cursor = null;
   522          try {
   523              cursor = getContentResolver().query(uri, proj, null, null, null);
   524              if (cursor == null) {
   525                  return null;
   526              }
   527              cursor.moveToFirst();
   528              int columnIndex = cursor.getColumnIndex(proj[0]);
   529              return cursor.getString(columnIndex); // might still be null
   530          } finally {
   531              if (cursor != null) {
   532                  cursor.close();
   533              }
   534          }
   535      }
   536  
   537      private final IUploadService.Stub service = new IUploadService.Stub() {
   538  
   539          @Override
   540          public int enqueueUploadList(List<Uri> uriList) throws RemoteException {
   541              startService(new Intent(UploadService.this, UploadService.class));
   542              Log.d(TAG, "enqueuing list of " + uriList.size() + " URIs");
   543              incrementFilesToUpload(uriList.size());
   544              int goodCount = 0;
   545              for (Uri uri : uriList) {
   546                  goodCount += enqueueSingleUri(uri) ? 1 : 0;
   547              }
   548              Log.d(TAG, "...goodCount = " + goodCount);
   549              return goodCount;
   550          }
   551  
   552          @Override
   553          public boolean enqueueUpload(Uri uri) throws RemoteException {
   554              startUploadService();
   555              incrementFilesToUpload(1);
   556              return enqueueSingleUri(uri);
   557          }
   558  
   559          private boolean enqueueSingleUri(Uri uri) throws RemoteException {
   560              long statSize = 0;
   561              {
   562                  ParcelFileDescriptor pfd = getFileDescriptor(uri);
   563                  if (pfd == null) {
   564                      incrementFilesToUpload(-1);
   565                      stopServiceIfEmpty();
   566                      return false;
   567                  }
   568  
   569                  try {
   570                      statSize = pfd.getStatSize();
   571                  } finally {
   572                      try {
   573                          pfd.close();
   574                      } catch (IOException e) {
   575                      }
   576                  }
   577              }
   578  
   579              String diskPath = pathOfURI(uri);
   580              if (diskPath == null) {
   581                  Log.e(TAG, "failed to find pathOfURI(" + uri + ")");
   582                  return false;
   583              }
   584              Log.d(TAG, "diskPath of " + uri + " = " + diskPath);
   585  
   586              QueuedFile qf = new QueuedFile(uri, statSize, diskPath);
   587  
   588              boolean needResume = false;
   589              synchronized (UploadService.this) {
   590                  if (mFileBytesRemain.containsKey(qf)) {
   591                      Log.d(TAG, "Dup blob enqueue, ignoring " + qf);
   592                      stopServiceIfEmpty();
   593                      return false;
   594                  }
   595                  Log.d(TAG, "Enqueueing blob: " + qf);
   596                  mFileBytesRemain.put(qf, qf.getSize());
   597                  mQueueList.add(qf);
   598  
   599                  if (mFileBytesRemain.size() == 1) {
   600                      mBytesTotal = 0;
   601                      mFilesTotal = 0;
   602                      mBytesUploaded = 0;
   603                      mFilesUploaded = 0;
   604                  }
   605                  mBytesTotal += qf.getSize();
   606                  mFilesTotal += 1;
   607                  needResume = !mUploading;
   608  
   609                  if (mUploadThread != null) {
   610                      mUploadThread.enqueueFile(qf);
   611                  }
   612              }
   613              broadcastFileStatus();
   614              broadcastByteStatus();
   615              if (needResume) {
   616                  resume();
   617              }
   618              return true;
   619          }
   620  
   621          @Override
   622          public boolean isUploading() throws RemoteException {
   623              synchronized (UploadService.this) {
   624                  return mUploading;
   625              }
   626          }
   627  
   628          @Override
   629          public void registerCallback(IStatusCallback cb) throws RemoteException {
   630              // TODO: permit multiple listeners? when need comes.
   631              synchronized (UploadService.this) {
   632                  if (cb == null) {
   633                      cb = DummyNullCallback.instance();
   634                  }
   635                  mCallback = cb;
   636              }
   637              broadcastAllState();
   638          }
   639  
   640          @Override
   641          public void unregisterCallback(IStatusCallback cb) throws RemoteException {
   642              synchronized (UploadService.this) {
   643                  mCallback = DummyNullCallback.instance();
   644              }
   645          }
   646  
   647          @Override
   648          public boolean resume() throws RemoteException {
   649              Log.d(TAG, "Resuming upload...");
   650              HostPort hp = mPrefs.hostPort();
   651              if (!hp.isValid()) {
   652                  setUploadStatusText("Upload server not configured.");
   653                  return false;
   654              }
   655  
   656              final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Camli Upload");
   657              final WifiManager.WifiLock wifiLock = mWifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "Camli Upload");
   658  
   659              synchronized (UploadService.this) {
   660                  if (mUploadThread != null) {
   661                      Log.d(TAG, "Already uploading; aborting resume.");
   662                      return false;
   663                  }
   664  
   665                  wakeLock.acquire();
   666                  wifiLock.acquire();
   667  
   668                  Notification n = new Notification(android.R.drawable.stat_sys_upload, "Uploading", System.currentTimeMillis());
   669                  n.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
   670                  PendingIntent pIntent = PendingIntent.getActivity(UploadService.this, 0, new Intent(UploadService.this, CamliActivity.class), 0);
   671                  n.setLatestEventInfo(UploadService.this, "Uploading", "Camlistore uploader running", pIntent);
   672                  mNotificationManager.notify(NOTIFY_ID_UPLOADING, n);
   673  
   674                  mUploading = true;
   675                  mUploadThread = new UploadThread(UploadService.this, hp, mPrefs.trustedCert(), mPrefs.username(), mPrefs.password());
   676                  mUploadThread.start();
   677  
   678                  // Start a thread to release the wakelock...
   679                  final Thread threadToWatch = mUploadThread;
   680                  new Thread("UploadThread-waiter") {
   681                      @Override
   682                      public void run() {
   683                          while (true) {
   684                              try {
   685                                  threadToWatch.join(10000); // 10 seconds
   686                              } catch (InterruptedException e) {
   687                                  Log.d(TAG, "Interrupt waiting for uploader thread.", e);
   688                              }
   689                              synchronized (UploadService.this) {
   690                                  if (threadToWatch.getState() == Thread.State.TERMINATED) {
   691                                      break;
   692                                  }
   693                                  if (threadToWatch == mUploadThread) {
   694                                      Log.d(TAG, "UploadThread-waiter still waiting.");
   695                                      continue;
   696                                  }
   697                              }
   698                              break;
   699                          }
   700                          Log.d(TAG, "UploadThread done; releasing the wakelock");
   701                          wakeLock.release();
   702                          wifiLock.release();
   703                          onUploadThreadEnded();
   704                      }
   705                  }.start();
   706              }
   707              mCallback.setUploading(true);
   708              return true;
   709          }
   710  
   711          @Override
   712          public boolean pause() throws RemoteException {
   713              synchronized (UploadService.this) {
   714                  if (mUploadThread != null) {
   715                      stopUploadThread();
   716                      return true;
   717                  }
   718                  return false;
   719              }
   720          }
   721  
   722          @Override
   723          public int queueSize() throws RemoteException {
   724              synchronized (UploadService.this) {
   725                  return mQueueList.size();
   726              }
   727          }
   728  
   729          @Override
   730          public void stopEverything() throws RemoteException {
   731              synchronized (UploadService.this) {
   732                  mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
   733                  mFileBytesRemain.clear();
   734                  mQueueList.clear();
   735                  mLastUploadStatusText = "Stopped";
   736                  mBytesInFlight = 0;
   737                  mFilesInFlight = 0;
   738                  mBytesTotal = 0;
   739                  mBytesUploaded = 0;
   740                  mFilesTotal = 0;
   741                  mFilesUploaded = 0;
   742                  stopUploadThread(); // recursive lock: okay
   743              }
   744              broadcastAllState();
   745          }
   746  
   747          @Override
   748          public void setBackgroundWatchersEnabled(boolean enabled) throws RemoteException {
   749              if (enabled) {
   750                  startUploadService();
   751                  UploadService.this.stopBackgroundWatchers();
   752                  UploadService.this.startBackgroundWatchers();
   753              } else {
   754                  UploadService.this.stopBackgroundWatchers();
   755                  stopServiceIfEmpty();
   756              }
   757          }
   758      };
   759  
   760      public void onChunkUploaded(CamputChunkUploadedMessage msg) {
   761          Log.d(TAG, "chunked uploaded for " + msg.queuedFile() + " with size " + msg.size());
   762          synchronized (UploadService.this) {
   763              incrBytes(msg.queuedFile(), msg.size());
   764          }
   765          broadcastAllState();
   766      }
   767  
   768      public void onStatReceived(String stat, long value) {
   769          String v;
   770          synchronized (UploadService.this) {
   771              if (stat == null) {
   772                  mStatValue.clear();
   773              } else {
   774                  mStatValue.put(stat, value);
   775              }
   776              StringBuilder sb = new StringBuilder();
   777              for (Entry<String, Long> ent : mStatValue.entrySet()) {
   778                  sb.append(ent.getKey());
   779                  sb.append(": ");
   780                  sb.append(ent.getValue());
   781                  sb.append("\n");
   782              }
   783              v = sb.toString();
   784              mLastUploadStatsText = v;
   785          }
   786          try {
   787              mCallback.setUploadStatsText(v);
   788          } catch (RemoteException e) {
   789          }
   790      }
   791  
   792      protected void stopUploadThread() {
   793          synchronized (UploadService.this) {
   794              mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
   795              if (mUploadThread != null) {
   796                  mUploadThread.stopUploads();
   797                  mUploadThread = null;
   798                  try {
   799                      mCallback.setUploading(false);
   800                  } catch (RemoteException e) {
   801                  }
   802              }
   803              mUploading = false;
   804          }
   805      }
   806  
   807      public void onStatsReceived(CamputStatsMessage msg) {
   808          synchronized (UploadService.this) {
   809              mBytesTotal = msg.totalBytes();
   810              mFilesTotal = (int) msg.totalFiles();
   811              mBytesUploaded = msg.skippedBytes() + msg.uploadedBytes();
   812              mFilesUploaded = (int) (msg.skippedFiles() + msg.uploadedFiles());
   813          }
   814          broadcastAllState();
   815      }
   816  }