7d2b92349a9d01e3a9a36aa0a9e3620fb29465c6
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / activity / Uploader.java
1 /**
2 * ownCloud Android client application
3 *
4 * @author Bartek Przybylski
5 * Copyright (C) 2012 Bartek Przybylski
6 * Copyright (C) 2015 ownCloud Inc.
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License version 2,
10 * as published by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22 package com.owncloud.android.ui.activity;
23
24 import java.io.File;
25 import java.io.FileDescriptor;
26 import java.io.FileInputStream;
27 import java.io.FileNotFoundException;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.LinkedList;
34 import java.util.List;
35 import java.util.Stack;
36 import java.util.Vector;
37 import java.util.concurrent.ExecutionException;
38
39 import android.accounts.Account;
40 import android.accounts.AccountManager;
41 import android.app.AlertDialog;
42 import android.app.AlertDialog.Builder;
43 import android.app.Dialog;
44 import android.app.ProgressDialog;
45 import android.content.Context;
46 import android.content.DialogInterface;
47 import android.content.DialogInterface.OnCancelListener;
48 import android.content.DialogInterface.OnClickListener;
49 import android.content.Intent;
50 import android.content.SharedPreferences;
51 import android.content.res.Resources.NotFoundException;
52 import android.database.Cursor;
53 import android.net.Uri;
54 import android.os.Bundle;
55 import android.os.ParcelFileDescriptor;
56 import android.os.Parcelable;
57 import android.preference.PreferenceManager;
58 import android.provider.MediaStore;
59 import android.provider.MediaStore.Audio;
60 import android.provider.MediaStore.Images;
61 import android.provider.MediaStore.Video;
62 import android.view.View;
63 import android.widget.AdapterView;
64 import android.widget.AdapterView.OnItemClickListener;
65 import android.widget.Button;
66 import android.widget.EditText;
67 import android.widget.ListView;
68 import android.widget.SimpleAdapter;
69 import android.widget.Toast;
70
71 import com.actionbarsherlock.app.ActionBar;
72 import com.actionbarsherlock.view.MenuItem;
73 import com.owncloud.android.MainApp;
74 import com.owncloud.android.R;
75 import com.owncloud.android.authentication.AccountAuthenticator;
76 import com.owncloud.android.datamodel.OCFile;
77 import com.owncloud.android.files.services.FileUploader;
78 import com.owncloud.android.lib.common.operations.RemoteOperation;
79 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
80 import com.owncloud.android.lib.common.utils.Log_OC;
81 import com.owncloud.android.operations.CreateFolderOperation;
82 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
83 import com.owncloud.android.utils.CopyTmpFileAsyncTask;
84 import com.owncloud.android.utils.DisplayUtils;
85 import com.owncloud.android.utils.ErrorMessageAdapter;
86 import com.owncloud.android.utils.FileStorageUtils;
87 import com.owncloud.android.utils.UriUtils;
88
89
90 /**
91 * This can be used to upload things to an ownCloud instance.
92 */
93 public class Uploader extends FileActivity
94 implements OnItemClickListener, android.view.View.OnClickListener,
95 CopyTmpFileAsyncTask.OnCopyTmpFileTaskListener {
96
97 private static final String TAG = Uploader.class.getSimpleName();
98
99 private AccountManager mAccountManager;
100 private Stack<String> mParents;
101 private ArrayList<Parcelable> mStreamsToUpload;
102 private boolean mCreateDir;
103 private String mUploadPath;
104 private OCFile mFile;
105 private boolean mAccountSelected;
106
107 private final static int DIALOG_NO_ACCOUNT = 0;
108 private final static int DIALOG_WAITING = 1;
109 private final static int DIALOG_NO_STREAM = 2;
110 private final static int DIALOG_MULTIPLE_ACCOUNT = 3;
111
112 private final static int REQUEST_CODE_SETUP_ACCOUNT = 0;
113
114 private final static String KEY_PARENTS = "PARENTS";
115 private final static String KEY_FILE = "FILE";
116 private final static String KEY_ACCOUNT_SELECTED = "ACCOUNT_SELECTED";
117
118 @Override
119 protected void onCreate(Bundle savedInstanceState) {
120 prepareStreamsToUpload();
121
122 if (savedInstanceState == null) {
123 mParents = new Stack<String>();
124 mAccountSelected = false;
125 } else {
126 mParents = (Stack<String>) savedInstanceState.getSerializable(KEY_PARENTS);
127 mFile = savedInstanceState.getParcelable(KEY_FILE);
128 mAccountSelected = savedInstanceState.getBoolean(KEY_ACCOUNT_SELECTED);
129 }
130 super.onCreate(savedInstanceState);
131
132 ActionBar actionBar = getSupportActionBar();
133 actionBar.setIcon(DisplayUtils.getSeasonalIconId());
134
135 }
136
137 @Override
138 protected void setAccount(Account account, boolean savedAccount) {
139 if (somethingToUpload()) {
140 mAccountManager = (AccountManager) getSystemService(Context.ACCOUNT_SERVICE);
141 Account[] accounts = mAccountManager.getAccountsByType(MainApp.getAccountType());
142 if (accounts.length == 0) {
143 Log_OC.i(TAG, "No ownCloud account is available");
144 showDialog(DIALOG_NO_ACCOUNT);
145 } else if (accounts.length > 1 && !mAccountSelected) {
146 Log_OC.i(TAG, "More than one ownCloud is available");
147 showDialog(DIALOG_MULTIPLE_ACCOUNT);
148 } else {
149 if (!savedAccount) {
150 setAccount(accounts[0]);
151 }
152 }
153
154 } else {
155 showDialog(DIALOG_NO_STREAM);
156 }
157
158 super.setAccount(account, savedAccount);
159 }
160
161 @Override
162 protected void onAccountSet(boolean stateWasRecovered) {
163 super.onAccountSet(mAccountWasRestored);
164 initTargetFolder();
165 populateDirectoryList();
166 }
167
168 @Override
169 protected void onSaveInstanceState(Bundle outState) {
170 Log_OC.d(TAG, "onSaveInstanceState() start");
171 super.onSaveInstanceState(outState);
172 outState.putSerializable(KEY_PARENTS, mParents);
173 //outState.putParcelable(KEY_ACCOUNT, mAccount);
174 outState.putParcelable(KEY_FILE, mFile);
175 outState.putBoolean(KEY_ACCOUNT_SELECTED, mAccountSelected);
176
177 Log_OC.d(TAG, "onSaveInstanceState() end");
178 }
179
180 @Override
181 protected Dialog onCreateDialog(final int id) {
182 final AlertDialog.Builder builder = new Builder(this);
183 switch (id) {
184 case DIALOG_WAITING:
185 ProgressDialog pDialog = new ProgressDialog(this);
186 pDialog.setIndeterminate(false);
187 pDialog.setCancelable(false);
188 pDialog.setMessage(getResources().getString(R.string.uploader_info_uploading));
189 return pDialog;
190 case DIALOG_NO_ACCOUNT:
191 builder.setIcon(android.R.drawable.ic_dialog_alert);
192 builder.setTitle(R.string.uploader_wrn_no_account_title);
193 builder.setMessage(String.format(
194 getString(R.string.uploader_wrn_no_account_text), getString(R.string.app_name)));
195 builder.setCancelable(false);
196 builder.setPositiveButton(R.string.uploader_wrn_no_account_setup_btn_text, new OnClickListener() {
197 @Override
198 public void onClick(DialogInterface dialog, int which) {
199 if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.ECLAIR_MR1) {
200 // using string value since in API7 this
201 // constatn is not defined
202 // in API7 < this constatant is defined in
203 // Settings.ADD_ACCOUNT_SETTINGS
204 // and Settings.EXTRA_AUTHORITIES
205 Intent intent = new Intent(android.provider.Settings.ACTION_ADD_ACCOUNT);
206 intent.putExtra("authorities", new String[] { MainApp.getAuthTokenType() });
207 startActivityForResult(intent, REQUEST_CODE_SETUP_ACCOUNT);
208 } else {
209 // since in API7 there is no direct call for
210 // account setup, so we need to
211 // show our own AccountSetupAcricity, get
212 // desired results and setup
213 // everything for ourself
214 Intent intent = new Intent(getBaseContext(), AccountAuthenticator.class);
215 startActivityForResult(intent, REQUEST_CODE_SETUP_ACCOUNT);
216 }
217 }
218 });
219 builder.setNegativeButton(R.string.uploader_wrn_no_account_quit_btn_text, new OnClickListener() {
220 @Override
221 public void onClick(DialogInterface dialog, int which) {
222 finish();
223 }
224 });
225 return builder.create();
226 case DIALOG_MULTIPLE_ACCOUNT:
227 CharSequence ac[] = new CharSequence[
228 mAccountManager.getAccountsByType(MainApp.getAccountType()).length];
229 for (int i = 0; i < ac.length; ++i) {
230 ac[i] = DisplayUtils.convertIdn(
231 mAccountManager.getAccountsByType(MainApp.getAccountType())[i].name, false);
232 }
233 builder.setTitle(R.string.common_choose_account);
234 builder.setItems(ac, new OnClickListener() {
235 @Override
236 public void onClick(DialogInterface dialog, int which) {
237 setAccount(mAccountManager.getAccountsByType(MainApp.getAccountType())[which]);
238 onAccountSet(mAccountWasRestored);
239 dialog.dismiss();
240 mAccountSelected = true;
241 }
242 });
243 builder.setCancelable(true);
244 builder.setOnCancelListener(new OnCancelListener() {
245 @Override
246 public void onCancel(DialogInterface dialog) {
247 dialog.cancel();
248 finish();
249 }
250 });
251 return builder.create();
252 case DIALOG_NO_STREAM:
253 builder.setIcon(android.R.drawable.ic_dialog_alert);
254 builder.setTitle(R.string.uploader_wrn_no_content_title);
255 builder.setMessage(R.string.uploader_wrn_no_content_text);
256 builder.setCancelable(false);
257 builder.setNegativeButton(R.string.common_cancel, new OnClickListener() {
258 @Override
259 public void onClick(DialogInterface dialog, int which) {
260 finish();
261 }
262 });
263 return builder.create();
264 default:
265 throw new IllegalArgumentException("Unknown dialog id: " + id);
266 }
267 }
268
269 class a implements OnClickListener {
270 String mPath;
271 EditText mDirname;
272
273 public a(String path, EditText dirname) {
274 mPath = path;
275 mDirname = dirname;
276 }
277
278 @Override
279 public void onClick(DialogInterface dialog, int which) {
280 Uploader.this.mUploadPath = mPath + mDirname.getText().toString();
281 Uploader.this.mCreateDir = true;
282 uploadFiles();
283 }
284 }
285
286 @Override
287 public void onBackPressed() {
288
289 if (mParents.size() <= 1) {
290 super.onBackPressed();
291 return;
292 } else {
293 mParents.pop();
294 populateDirectoryList();
295 }
296 }
297
298 @Override
299 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
300 // click on folder in the list
301 Log_OC.d(TAG, "on item click");
302 Vector<OCFile> tmpfiles = getStorageManager().getFolderContent(mFile);
303 if (tmpfiles.size() <= 0) return;
304 // filter on dirtype
305 Vector<OCFile> files = new Vector<OCFile>();
306 for (OCFile f : tmpfiles)
307 if (f.isFolder())
308 files.add(f);
309 if (files.size() < position) {
310 throw new IndexOutOfBoundsException("Incorrect item selected");
311 }
312 mParents.push(files.get(position).getFileName());
313 populateDirectoryList();
314 }
315
316 @Override
317 public void onClick(View v) {
318 // click on button
319 switch (v.getId()) {
320 case R.id.uploader_choose_folder:
321 mUploadPath = ""; // first element in mParents is root dir, represented by "";
322 // init mUploadPath with "/" results in a "//" prefix
323 for (String p : mParents)
324 mUploadPath += p + OCFile.PATH_SEPARATOR;
325 Log_OC.d(TAG, "Uploading file to dir " + mUploadPath);
326
327 uploadFiles();
328
329 break;
330
331 case R.id.uploader_new_folder:
332 CreateFolderDialogFragment dialog = CreateFolderDialogFragment.newInstance(mFile);
333 dialog.show(getSupportFragmentManager(), "createdirdialog");
334 break;
335
336
337 default:
338 throw new IllegalArgumentException("Wrong element clicked");
339 }
340 }
341
342 @Override
343 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
344 super.onActivityResult(requestCode, resultCode, data);
345 Log_OC.i(TAG, "result received. req: " + requestCode + " res: " + resultCode);
346 if (requestCode == REQUEST_CODE_SETUP_ACCOUNT) {
347 dismissDialog(DIALOG_NO_ACCOUNT);
348 if (resultCode == RESULT_CANCELED) {
349 finish();
350 }
351 Account[] accounts = mAccountManager.getAccountsByType(MainApp.getAuthTokenType());
352 if (accounts.length == 0) {
353 showDialog(DIALOG_NO_ACCOUNT);
354 } else {
355 // there is no need for checking for is there more then one
356 // account at this point
357 // since account setup can set only one account at time
358 setAccount(accounts[0]);
359 populateDirectoryList();
360 }
361 }
362 }
363
364 private void populateDirectoryList() {
365 setContentView(R.layout.uploader_layout);
366
367 ListView mListView = (ListView) findViewById(android.R.id.list);
368
369 String current_dir = mParents.peek();
370 if(current_dir.equals("")){
371 getSupportActionBar().setTitle(getString(R.string.default_display_name_for_root_folder));
372 }
373 else{
374 getSupportActionBar().setTitle(current_dir);
375 }
376 boolean notRoot = (mParents.size() > 1);
377 ActionBar actionBar = getSupportActionBar();
378 actionBar.setDisplayHomeAsUpEnabled(notRoot);
379 actionBar.setHomeButtonEnabled(notRoot);
380
381 String full_path = generatePath(mParents);
382
383 Log_OC.d(TAG, "Populating view with content of : " + full_path);
384
385 mFile = getStorageManager().getFileByPath(full_path);
386 if (mFile != null) {
387 Vector<OCFile> files = getStorageManager().getFolderContent(mFile);
388 List<HashMap<String, Object>> data = new LinkedList<HashMap<String,Object>>();
389 for (OCFile f : files) {
390 HashMap<String, Object> h = new HashMap<String, Object>();
391 if (f.isFolder()) {
392 h.put("dirname", f.getFileName());
393 data.add(h);
394 }
395 }
396 SimpleAdapter sa = new SimpleAdapter(this,
397 data,
398 R.layout.uploader_list_item_layout,
399 new String[] {"dirname"},
400 new int[] {R.id.textView1});
401
402 mListView.setAdapter(sa);
403 Button btnChooseFolder = (Button) findViewById(R.id.uploader_choose_folder);
404 btnChooseFolder.setOnClickListener(this);
405
406 Button btnNewFolder = (Button) findViewById(R.id.uploader_new_folder);
407 btnNewFolder.setOnClickListener(this);
408
409 mListView.setOnItemClickListener(this);
410 }
411 }
412
413 private String generatePath(Stack<String> dirs) {
414 String full_path = "";
415
416 for (String a : dirs)
417 full_path += a + "/";
418 return full_path;
419 }
420
421 private void prepareStreamsToUpload() {
422 if (getIntent().getAction().equals(Intent.ACTION_SEND)) {
423 mStreamsToUpload = new ArrayList<Parcelable>();
424 mStreamsToUpload.add(getIntent().getParcelableExtra(Intent.EXTRA_STREAM));
425 } else if (getIntent().getAction().equals(Intent.ACTION_SEND_MULTIPLE)) {
426 mStreamsToUpload = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
427 }
428 }
429
430 private boolean somethingToUpload() {
431 return (mStreamsToUpload != null && mStreamsToUpload.get(0) != null);
432 }
433
434 public void uploadFiles() {
435 try {
436
437 ArrayList<String> local = new ArrayList<String>();
438 ArrayList<String> remote = new ArrayList<String>();
439
440 // this checks the mimeType
441 for (Parcelable mStream : mStreamsToUpload) {
442
443 Uri uri = (Uri) mStream;
444 String data = null;
445 String filePath = "";
446
447 if (uri != null) {
448 if (uri.getScheme().equals("content")) {
449 String mimeType = getContentResolver().getType(uri);
450
451 if (mimeType.contains("image")) {
452 String[] CONTENT_PROJECTION = { Images.Media.DATA,
453 Images.Media.DISPLAY_NAME, Images.Media.MIME_TYPE,
454 Images.Media.SIZE };
455 Cursor c = getContentResolver().query(uri, CONTENT_PROJECTION, null,
456 null, null);
457 c.moveToFirst();
458 int index = c.getColumnIndex(Images.Media.DATA);
459 data = c.getString(index);
460 filePath = mUploadPath +
461 c.getString(c.getColumnIndex(Images.Media.DISPLAY_NAME));
462
463 } else if (mimeType.contains("video")) {
464 String[] CONTENT_PROJECTION = { Video.Media.DATA,
465 Video.Media.DISPLAY_NAME, Video.Media.MIME_TYPE,
466 Video.Media.SIZE, Video.Media.DATE_MODIFIED };
467 Cursor c = getContentResolver().query(uri, CONTENT_PROJECTION, null,
468 null, null);
469 c.moveToFirst();
470 int index = c.getColumnIndex(Video.Media.DATA);
471 data = c.getString(index);
472 filePath = mUploadPath +
473 c.getString(c.getColumnIndex(Video.Media.DISPLAY_NAME));
474
475 } else if (mimeType.contains("audio")) {
476 String[] CONTENT_PROJECTION = { Audio.Media.DATA,
477 Audio.Media.DISPLAY_NAME, Audio.Media.MIME_TYPE,
478 Audio.Media.SIZE };
479 Cursor c = getContentResolver().query(uri, CONTENT_PROJECTION, null,
480 null, null);
481 c.moveToFirst();
482 int index = c.getColumnIndex(Audio.Media.DATA);
483 data = c.getString(index);
484 filePath = mUploadPath +
485 c.getString(c.getColumnIndex(Audio.Media.DISPLAY_NAME));
486
487 } else {
488 Cursor cursor = getContentResolver().query(uri,
489 new String[]{MediaStore.MediaColumns.DISPLAY_NAME},
490 null, null, null);
491 cursor.moveToFirst();
492 int nameIndex = cursor.getColumnIndex(cursor.getColumnNames()[0]);
493 if (nameIndex >= 0) {
494 filePath = mUploadPath + cursor.getString(nameIndex);
495 }
496 }
497 if (data == null) {
498 CopyTmpFileAsyncTask copyTask = new CopyTmpFileAsyncTask(this);
499 Object[] params = { uri, filePath };
500 try {
501 data = copyTask.execute(params).get();
502 } catch (ExecutionException e) {
503 Log_OC.e(TAG, "ExecutionException " + e);
504 } catch (InterruptedException e) {
505 Log_OC.e(TAG, "InterruptedException " + e);
506 }
507 }
508
509 local.add(data);
510 remote.add(filePath);
511
512 } else if (uri.getScheme().equals("file")) {
513 filePath = Uri.decode(uri.toString()).replace(uri.getScheme() +
514 "://", "");
515 if (filePath.contains("mnt")) {
516 String splitedFilePath[] = filePath.split("/mnt");
517 filePath = splitedFilePath[1];
518 }
519 final File file = new File(filePath);
520 local.add(file.getAbsolutePath());
521 remote.add(mUploadPath + file.getName());
522 }
523 else {
524 throw new SecurityException();
525 }
526 }
527 else {
528 throw new SecurityException();
529 }
530
531 Intent intent = new Intent(getApplicationContext(), FileUploader.class);
532 intent.putExtra(FileUploader.KEY_UPLOAD_TYPE, FileUploader.UPLOAD_MULTIPLE_FILES);
533 intent.putExtra(FileUploader.KEY_LOCAL_FILE, local.toArray(new String[local.size()]));
534 intent.putExtra(FileUploader.KEY_REMOTE_FILE,
535 remote.toArray(new String[remote.size()]));
536 intent.putExtra(FileUploader.KEY_ACCOUNT, getAccount());
537 startService(intent);
538
539 //Save the path to shared preferences
540 SharedPreferences.Editor appPrefs = PreferenceManager
541 .getDefaultSharedPreferences(getApplicationContext()).edit();
542 appPrefs.putString("last_upload_path", mUploadPath);
543 appPrefs.apply();
544
545 finish();
546 }
547
548 } catch (SecurityException e) {
549 String message = String.format(getString(R.string.uploader_error_forbidden_content),
550 getString(R.string.app_name));
551 Toast.makeText(this, message, Toast.LENGTH_LONG).show();
552 }
553 }
554
555 @Override
556 public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
557 super.onRemoteOperationFinish(operation, result);
558
559
560 if (operation instanceof CreateFolderOperation) {
561 onCreateFolderOperationFinish((CreateFolderOperation)operation, result);
562 }
563
564 }
565
566 /**
567 * Updates the view associated to the activity after the finish of an operation
568 * trying create a new folder
569 *
570 * @param operation Creation operation performed.
571 * @param result Result of the creation.
572 */
573 private void onCreateFolderOperationFinish(CreateFolderOperation operation,
574 RemoteOperationResult result) {
575 if (result.isSuccess()) {
576 dismissLoadingDialog();
577 populateDirectoryList();
578 } else {
579 dismissLoadingDialog();
580 try {
581 Toast msg = Toast.makeText(this,
582 ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()),
583 Toast.LENGTH_LONG);
584 msg.show();
585
586 } catch (NotFoundException e) {
587 Log_OC.e(TAG, "Error while trying to show fail message " , e);
588 }
589 }
590 }
591
592
593 /**
594 * Loads the target folder initialize shown to the user.
595 *
596 * The target account has to be chosen before this method is called.
597 */
598 private void initTargetFolder() {
599 if (getStorageManager() == null) {
600 throw new IllegalStateException("Do not call this method before " +
601 "initializing mStorageManager");
602 }
603
604 SharedPreferences appPreferences = PreferenceManager
605 .getDefaultSharedPreferences(getApplicationContext());
606
607 String last_path = appPreferences.getString("last_upload_path", "");
608 // "/" equals root-directory
609 if(last_path.equals("/")) {
610 mParents.add("");
611 } else{
612 String[] dir_names = last_path.split("/");
613 for (String dir : dir_names)
614 mParents.add(dir);
615 }
616 //Make sure that path still exists, if it doesn't pop the stack and try the previous path
617 while(!getStorageManager().fileExists(generatePath(mParents)) && mParents.size() > 1){
618 mParents.pop();
619 }
620 }
621
622
623 @Override
624 public boolean onOptionsItemSelected(MenuItem item) {
625 boolean retval = true;
626 switch (item.getItemId()) {
627 case android.R.id.home:
628 if((mParents.size() > 1)) {
629 onBackPressed();
630 }
631 break;
632
633 default:
634 retval = super.onOptionsItemSelected(item);
635 }
636 return retval;
637 }
638
639
640 /**
641 * Process the result of CopyTmpFileAsyncTask
642 * @param result
643 */
644 @Override
645 public void OnCopyTmpFileTaskListener(String result) {
646
647 }
648
649 }