From 4551db6ad49385acc5a101131bc2c0de2dd58fef Mon Sep 17 00:00:00 2001 From: Chirag Maheshwari Date: Fri, 14 Jul 2017 14:25:06 +0530 Subject: [PATCH 1/8] Upload and Delete file api and implementation (#242) * feat: adds delete file functionality * fix: change api route for file delete * adds basic file upload functionality * fix image null pointer error and switch to query params * adds file upload progress event * adds permission handler before uploading files * adds check for duplicate file upload * fix file not found bug * adds feature to upload video files too * refactor code * adds bottom sheet for displaying upload options * adds upload click listeners and callbacks in ServerFilesActivity * implements camera upload functionality * minor fixes and code clean up * removes upload icon from title bar --- build.gradle | 2 + src/main/AndroidManifest.xml | 15 +- .../activity/ServerFilesActivity.java | 309 +++++++++++++++++- .../adapter/FilesFilterBaseAdapter.java | 7 +- .../adapter/UploadOptionsAdapter.java | 90 +++++ .../anywhere/bus/ServerFileDeleteEvent.java | 32 ++ .../bus/ServerFileUploadCompleteEvent.java | 32 ++ .../bus/ServerFileUploadProgressEvent.java | 32 ++ .../amahi/anywhere/bus/UploadClickEvent.java | 35 ++ .../fragment/ServerFilesFragment.java | 226 +++++++++---- .../anywhere/fragment/UploadBottomSheet.java | 86 +++++ .../amahi/anywhere/model/UploadOption.java | 60 ++++ .../amahi/anywhere/server/api/ServerApi.java | 61 ++-- .../anywhere/server/client/ServerClient.java | 62 ++-- .../response/ServerFileDeleteResponse.java | 48 +++ .../response/ServerFileUploadResponse.java | 49 +++ .../java/org/amahi/anywhere/util/Android.java | 4 + .../java/org/amahi/anywhere/util/Intents.java | 18 + .../anywhere/util/ProgressRequestBody.java | 98 ++++++ src/main/res/drawable/ic_add.xml | 9 + src/main/res/drawable/ic_camera.xml | 12 + src/main/res/drawable/ic_cloud_upload.xml | 9 + src/main/res/drawable/ic_delete.xml | 13 + src/main/res/layout/activity_server_files.xml | 22 +- src/main/res/layout/upload_bottom_sheet.xml | 28 ++ src/main/res/layout/upload_list_item.xml | 26 ++ src/main/res/menu/action_bar_server_files.xml | 6 +- .../res/menu/action_mode_server_files.xml | 11 +- src/main/res/values/strings.xml | 29 +- src/main/res/xml/file_paths.xml | 6 + 30 files changed, 1298 insertions(+), 139 deletions(-) create mode 100644 src/main/java/org/amahi/anywhere/adapter/UploadOptionsAdapter.java create mode 100644 src/main/java/org/amahi/anywhere/bus/ServerFileDeleteEvent.java create mode 100644 src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java create mode 100644 src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java create mode 100644 src/main/java/org/amahi/anywhere/bus/UploadClickEvent.java create mode 100644 src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java create mode 100644 src/main/java/org/amahi/anywhere/model/UploadOption.java create mode 100644 src/main/java/org/amahi/anywhere/server/response/ServerFileDeleteResponse.java create mode 100644 src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java create mode 100644 src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java create mode 100644 src/main/res/drawable/ic_add.xml create mode 100644 src/main/res/drawable/ic_camera.xml create mode 100644 src/main/res/drawable/ic_cloud_upload.xml create mode 100644 src/main/res/drawable/ic_delete.xml create mode 100644 src/main/res/layout/upload_bottom_sheet.xml create mode 100644 src/main/res/layout/upload_list_item.xml create mode 100644 src/main/res/xml/file_paths.xml diff --git a/build.gradle b/build.gradle index 426c54358..e1a1d5bde 100644 --- a/build.gradle +++ b/build.gradle @@ -99,6 +99,7 @@ def formatStringField(field) { dependencies { repositories { + jcenter() mavenCentral() mavenLocal() maven { url 'https://maven.fabric.io/public' } @@ -130,6 +131,7 @@ dependencies { compile 'com.squareup.retrofit2:converter-gson:2.2.0' compile 'com.squareup.okhttp3:logging-interceptor:3.6.0' compile 'org.videolan:libvlc:2.1.1' + compile 'pub.devrel:easypermissions:0.4.2' testCompile 'org.robolectric:robolectric:3.1.2' testCompile 'junit:junit:4.12' testCompile 'org.robolectric:shadows-multidex:3.0' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 8f67351b8..af0f9124c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -18,7 +18,6 @@ --> @@ -47,6 +46,10 @@ + + @@ -63,6 +66,16 @@ android:theme="@style/Theme.Amahi" android:banner="@drawable/tv_banner"> + + + + diff --git a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java index 781d21d46..533d4e203 100644 --- a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java +++ b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java @@ -19,13 +19,26 @@ package org.amahi.anywhere.activity; +import android.Manifest; import android.app.DialogFragment; -import android.support.v4.app.Fragment; +import android.app.ProgressDialog; +import android.content.DialogInterface; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.annotation.RequiresApi; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v4.app.Fragment; +import android.support.v4.content.FileProvider; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.MenuItem; +import android.view.View; import com.squareup.otto.Subscribe; @@ -35,45 +48,69 @@ import org.amahi.anywhere.bus.FileDownloadedEvent; import org.amahi.anywhere.bus.FileOpeningEvent; import org.amahi.anywhere.bus.ServerFileSharingEvent; -import org.amahi.anywhere.fragment.ServerFileDownloadingFragment; +import org.amahi.anywhere.bus.ServerFileUploadCompleteEvent; +import org.amahi.anywhere.bus.ServerFileUploadProgressEvent; +import org.amahi.anywhere.bus.UploadClickEvent; import org.amahi.anywhere.fragment.GooglePlaySearchFragment; +import org.amahi.anywhere.fragment.ServerFileDownloadingFragment; +import org.amahi.anywhere.fragment.ServerFilesFragment; +import org.amahi.anywhere.fragment.UploadBottomSheet; +import org.amahi.anywhere.model.UploadOption; import org.amahi.anywhere.server.client.ServerClient; import org.amahi.anywhere.server.model.ServerFile; import org.amahi.anywhere.server.model.ServerShare; +import org.amahi.anywhere.util.Android; import org.amahi.anywhere.util.Fragments; import org.amahi.anywhere.util.Intents; import org.amahi.anywhere.util.Mimes; +import java.io.File; +import java.io.IOException; +import java.util.Date; import java.util.List; import javax.inject.Inject; +import pub.devrel.easypermissions.AppSettingsDialog; +import pub.devrel.easypermissions.EasyPermissions; +import timber.log.Timber; + /** * Files activity. Shows files navigation and operates basic file actions, * such as opening and sharing. * The files navigation itself is done via {@link org.amahi.anywhere.fragment.ServerFilesFragment}. */ -public class ServerFilesActivity extends AppCompatActivity -{ - private static final class State - { +public class ServerFilesActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks { + + private static final class State { + private State() { } public static final String FILE = "file"; + public static final String FILE_ACTION = "file_action"; } - private static enum FileAction - { - OPEN, SHARE + + private enum FileAction { + OPEN, SHARE; } @Inject ServerClient serverClient; private ServerFile file; + private FileAction fileAction; + private ProgressDialog uploadProgressDialog; + + private static final int FILE_UPLOAD_PERMISSION = 102; + private static final int CAMERA_PERMISSION = 103; + private static final int REQUEST_UPLOAD_IMAGE = 201; + private static final int REQUEST_CAMERA_IMAGE = 202; + + private File cameraImage; @Override protected void onCreate(Bundle savedInstanceState) { @@ -98,6 +135,8 @@ private void setUpHomeNavigation() { private void setUpFiles(Bundle state) { setUpFilesTitle(); + setUpUploadFAB(); + setUpUploadDialog(); setUpFilesFragment(); setUpFilesState(state); } @@ -106,13 +145,32 @@ private void setUpFilesTitle() { getSupportActionBar().setTitle(getShare().getName()); } - @Override - public void onBackPressed() { - super.onBackPressed(); - setUpFilesTitle(); - } - private ServerShare getShare() { + private void setUpUploadFAB() { + final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab_upload); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new UploadBottomSheet().show(getSupportFragmentManager(), "upload_dialog"); + } + }); + } + + private void setUpUploadDialog() { + uploadProgressDialog = new ProgressDialog(this); + uploadProgressDialog.setTitle(getString(R.string.message_file_upload_title)); + uploadProgressDialog.setCancelable(false); + uploadProgressDialog.setIndeterminate(false); + uploadProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + setUpFilesTitle(); + } + + private ServerShare getShare() { return getIntent().getParcelableExtra(Intents.Extras.SERVER_SHARE); } @@ -226,6 +284,225 @@ private void showGooglePlaySearchFragment(ServerFile file) { fragment.show(getFragmentManager(), GooglePlaySearchFragment.TAG); } + @Override + public void onPermissionsGranted(int requestCode, List perms) { + if (requestCode == FILE_UPLOAD_PERMISSION) { + showFileChooser(); + } else if (requestCode == CAMERA_PERMISSION) { + openCamera(); + } + } + + @Override + public void onPermissionsDenied(int requestCode, List perms) { + if (requestCode == FILE_UPLOAD_PERMISSION) { + showPermissionSnackBar(getString(R.string.file_upload_permission_denied)); + } else if (requestCode == CAMERA_PERMISSION) { + showPermissionSnackBar(getString(R.string.file_upload_permission_denied)); + } + } + + private View getParentView() { + return findViewById(R.id.coordinator_files); + } + + private void showPermissionSnackBar(String message) { + Snackbar.make(getParentView(), message, Snackbar.LENGTH_LONG) + .setAction(R.string.menu_settings, new View.OnClickListener() { + @Override + public void onClick(View v) { + new AppSettingsDialog.Builder(ServerFilesActivity.this).build().show(); + } + }) + .show(); + } + + @Subscribe + public void onUploadOptionClick(UploadClickEvent event) { + int option = event.getUploadOption(); + switch (option) { + case UploadOption.CAMERA: + if (Android.isPermissionRequired()) { + checkCameraPermissions(); + } else { + openCamera(); + } + break; + case UploadOption.FILE: + if (Android.isPermissionRequired()) { + checkFileReadPermissions(); + } else { + showFileChooser(); + } + break; + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private void checkCameraPermissions() { + String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE}; + if (EasyPermissions.hasPermissions(this, perms)) { + openCamera(); + } else { + EasyPermissions.requestPermissions(this, getString(R.string.camera_permission), + CAMERA_PERMISSION, perms); + } + } + + private void openCamera() { + Intent cameraIntent = Intents.Builder.with(this).buildCameraIntent(); + if (cameraIntent.resolveActivity(getPackageManager()) != null) { + cameraImage = null; + try { + cameraImage = createImageFile(); + Uri photoURI = FileProvider.getUriForFile(this, + "org.amahi.anywhere.fileprovider", cameraImage); + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); + startActivityForResult(cameraIntent, REQUEST_CAMERA_IMAGE); + } catch (IOException ex) { + Timber.d(ex); + } + } + } + + private File createImageFile() throws IOException { + String timeStamp = String.valueOf(new Date().getTime()); + String imageFileName = "photo-" + timeStamp; + File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + return File.createTempFile(imageFileName, ".jpg", storageDir); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private void checkFileReadPermissions() { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + if (EasyPermissions.hasPermissions(this, perms)) { + showFileChooser(); + } else { + EasyPermissions.requestPermissions(this, getString(R.string.file_upload_permission), + FILE_UPLOAD_PERMISSION, perms); + } + } + + private void showFileChooser() { + Intent intent = Intents.Builder.with(this).buildMediaPickerIntent(); + this.startActivityForResult(intent, REQUEST_UPLOAD_IMAGE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + switch (requestCode) { + case REQUEST_UPLOAD_IMAGE: + if (data != null) { + Uri selectedImageUri = data.getData(); + String filePath = querySelectedImagePath(selectedImageUri); + if (filePath != null) { + File file = new File(filePath); + ServerFilesFragment fragment = (ServerFilesFragment) + getSupportFragmentManager() + .findFragmentById(R.id.container_files); + if (fragment.checkForDuplicateFile(file.getName())) { + showDuplicateFileUploadDialog(file); + } else { + uploadFile(file); + } + } + } + break; + case REQUEST_CAMERA_IMAGE: + if (cameraImage.exists()) { + uploadFile(cameraImage); + } + break; + } + } + } + + private String querySelectedImagePath(Uri selectedImageUri) { + String filePath = null; + if ("content".equals(selectedImageUri.getScheme())) { + String[] filePathColumn = {MediaStore.Images.Media.DATA}; + Cursor cursor = this.getContentResolver() + .query(selectedImageUri, filePathColumn, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + filePath = cursor.getString(columnIndex); + cursor.close(); + } + } else { + filePath = selectedImageUri.toString(); + } + return filePath; + } + + private void showDuplicateFileUploadDialog(final File file) { + new AlertDialog.Builder(this) + .setTitle(R.string.message_duplicate_file_upload) + .setMessage(getString(R.string.message_duplicate_file_upload_body, file.getName())) + .setPositiveButton(R.string.button_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + uploadFile(file); + } + }) + .setNegativeButton(R.string.button_no, null) + .show(); + } + + private void uploadFile(File uploadFile) { + serverClient.uploadFile(uploadFile, getShare(), file); + uploadProgressDialog.show(); + } + + + @Subscribe + public void onFileUploadProgressEvent(ServerFileUploadProgressEvent fileUploadProgressEvent) { + if (uploadProgressDialog.isShowing()) { + uploadProgressDialog.setProgress(fileUploadProgressEvent.getProgress()); + } + } + + @Subscribe + public void onFileUploadCompleteEvent(ServerFileUploadCompleteEvent event) { + uploadProgressDialog.dismiss(); + if (event.wasUploadSuccessful()) { + Fragments.Operator.at(this).replace(buildFilesFragment(getShare(), file), R.id.container_files); + Snackbar.make(getParentView(), R.string.message_file_upload_complete, Snackbar.LENGTH_LONG).show(); + if (cameraImage != null && cameraImage.exists()) { + clearCameraImage(); + } + } else { + Snackbar snackbar = Snackbar.make(getParentView(), R.string.message_file_upload_error, Snackbar.LENGTH_LONG); + if (cameraImage != null && cameraImage.exists()) { + snackbar + .setAction(R.string.button_retry, new View.OnClickListener() { + @Override + public void onClick(View v) { + uploadFile(cameraImage); + } + }) + .addCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar transientBottomBar, int event) { + super.onDismissed(transientBottomBar, event); + if (event != DISMISS_EVENT_ACTION) { + clearCameraImage(); + } + } + }); + } + snackbar.show(); + } + } + + private void clearCameraImage() { + //noinspection ResultOfMethodCallIgnored + cameraImage.delete(); + cameraImage = null; + } + @Subscribe public void onFileSharing(ServerFileSharingEvent event) { this.file = event.getFile(); @@ -264,7 +541,7 @@ protected void onPause() { BusProvider.getBus().unregister(this); } - @Override + @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/src/main/java/org/amahi/anywhere/adapter/FilesFilterBaseAdapter.java b/src/main/java/org/amahi/anywhere/adapter/FilesFilterBaseAdapter.java index ca3c2a314..11b38b95e 100644 --- a/src/main/java/org/amahi/anywhere/adapter/FilesFilterBaseAdapter.java +++ b/src/main/java/org/amahi/anywhere/adapter/FilesFilterBaseAdapter.java @@ -21,7 +21,6 @@ import android.graphics.Color; import android.net.Uri; -import android.support.annotation.DrawableRes; import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.View; @@ -34,7 +33,6 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import org.amahi.anywhere.R; import org.amahi.anywhere.server.client.ServerClient; import org.amahi.anywhere.server.model.ServerFile; import org.amahi.anywhere.server.model.ServerShare; @@ -113,6 +111,11 @@ public void replaceWith(ServerShare serverShare, List files) { notifyDataSetChanged(); } + public void removeFile(int position) { + this.files.remove(position); + notifyDataSetChanged(); + } + @Override public Filter getFilter() { if (filesFilter == null) { diff --git a/src/main/java/org/amahi/anywhere/adapter/UploadOptionsAdapter.java b/src/main/java/org/amahi/anywhere/adapter/UploadOptionsAdapter.java new file mode 100644 index 000000000..c36f49e41 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/adapter/UploadOptionsAdapter.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import org.amahi.anywhere.R; +import org.amahi.anywhere.fragment.UploadBottomSheet; +import org.amahi.anywhere.model.UploadOption; + +import java.util.ArrayList; + +/** + * Upload options adapter. + * for the {@link UploadBottomSheet}. + */ +public class UploadOptionsAdapter extends BaseAdapter { + private ArrayList uploadOptions; + private LayoutInflater inflater; + + static class ViewHolder { + ImageView image; + TextView text; + } + + public UploadOptionsAdapter(Context context, ArrayList uploadOptions) { + this.uploadOptions = uploadOptions; + this.inflater = LayoutInflater.from(context); + } + + @Override + public int getCount() { + return uploadOptions.size(); + } + + @Override + public Object getItem(int position) { + return uploadOptions.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + UploadOption uploadOption = uploadOptions.get(position); + + if (convertView == null) { + holder = new ViewHolder(); + convertView = inflater.inflate(R.layout.upload_list_item, parent, false); + convertView.setTag(holder); + + holder.image = (ImageView) convertView.findViewById(R.id.option_icon); + holder.text = (TextView) convertView.findViewById(R.id.option_text); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + holder.image.setImageResource(uploadOption.getIcon()); + holder.text.setText(uploadOption.getName()); + + return convertView; + } +} diff --git a/src/main/java/org/amahi/anywhere/bus/ServerFileDeleteEvent.java b/src/main/java/org/amahi/anywhere/bus/ServerFileDeleteEvent.java new file mode 100644 index 000000000..f90ca16f5 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/bus/ServerFileDeleteEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.bus; + +public class ServerFileDeleteEvent implements BusEvent { + private boolean isDeleted; + + public ServerFileDeleteEvent(boolean isDeleted) { + this.isDeleted = isDeleted; + } + + public boolean isDeleted() { + return isDeleted; + } +} diff --git a/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java new file mode 100644 index 000000000..2e45052d3 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.bus; + +public class ServerFileUploadCompleteEvent implements BusEvent { + private boolean wasUploadSuccessful; + + public ServerFileUploadCompleteEvent(boolean wasUploadSuccessful) { + this.wasUploadSuccessful = wasUploadSuccessful; + } + + public boolean wasUploadSuccessful() { + return wasUploadSuccessful; + } +} diff --git a/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java new file mode 100644 index 000000000..c57391fe6 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.bus; + +public class ServerFileUploadProgressEvent implements BusEvent { + private int progress; + + public ServerFileUploadProgressEvent(int progress) { + this.progress = progress; + } + + public int getProgress() { + return this.progress; + } +} diff --git a/src/main/java/org/amahi/anywhere/bus/UploadClickEvent.java b/src/main/java/org/amahi/anywhere/bus/UploadClickEvent.java new file mode 100644 index 000000000..193e8d08c --- /dev/null +++ b/src/main/java/org/amahi/anywhere/bus/UploadClickEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.bus; + +import org.amahi.anywhere.model.UploadOption; + +public class UploadClickEvent implements BusEvent { + private int uploadOption; + + public UploadClickEvent(@UploadOption.Types int uploadOption) { + this.uploadOption = uploadOption; + } + + @UploadOption.Types + public int getUploadOption() { + return uploadOption; + } +} diff --git a/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java index d17d4ea78..dda0161ce 100644 --- a/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java +++ b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java @@ -20,19 +20,19 @@ package org.amahi.anywhere.fragment; import android.Manifest; +import android.app.ProgressDialog; import android.app.SearchManager; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; +import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; -import android.provider.Settings; +import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.SearchView; import android.view.ActionMode; import android.view.LayoutInflater; @@ -47,6 +47,7 @@ import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.TextView; +import android.widget.Toast; import com.squareup.otto.Subscribe; @@ -58,12 +59,14 @@ import org.amahi.anywhere.adapter.ServerFilesMetadataAdapter; import org.amahi.anywhere.bus.BusProvider; import org.amahi.anywhere.bus.FileOpeningEvent; +import org.amahi.anywhere.bus.ServerFileDeleteEvent; import org.amahi.anywhere.bus.ServerFileSharingEvent; import org.amahi.anywhere.bus.ServerFilesLoadFailedEvent; import org.amahi.anywhere.bus.ServerFilesLoadedEvent; import org.amahi.anywhere.server.client.ServerClient; import org.amahi.anywhere.server.model.ServerFile; import org.amahi.anywhere.server.model.ServerShare; +import org.amahi.anywhere.util.Android; import org.amahi.anywhere.util.Fragments; import org.amahi.anywhere.util.Mimes; import org.amahi.anywhere.util.ViewDirector; @@ -76,24 +79,27 @@ import javax.inject.Inject; -import static android.support.v4.content.PermissionChecker.checkSelfPermission; +import pub.devrel.easypermissions.AppSettingsDialog; +import pub.devrel.easypermissions.EasyPermissions; /** * Files fragment. Shows files list. */ public class ServerFilesFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener, - AdapterView.OnItemClickListener, - AdapterView.OnItemLongClickListener, - ActionMode.Callback, - SearchView.OnQueryTextListener, - FilesFilterBaseAdapter.onFilterListChange -{ + AdapterView.OnItemClickListener, + AdapterView.OnItemLongClickListener, + ActionMode.Callback, + SearchView.OnQueryTextListener, + FilesFilterBaseAdapter.onFilterListChange, + EasyPermissions.PermissionCallbacks { private SearchView searchView; private MenuItem searchMenuItem; private LinearLayout mErrorLinearLayout; + private ProgressDialog deleteProgressDialog; + private int deleteFilePosition; + private int lastCheckedFileIndex = -1; - private static final class State - { + private static final class State { private State() { } @@ -101,10 +107,9 @@ private State() { public static final String FILES_SORT = "files_sort"; } - private static final int CALLBACK_NUMBER = 100; + private static final int SHARE_PERMISSIONS = 101; - private enum FilesSort - { + private enum FilesSort { NAME, MODIFICATION_TIME } @@ -134,6 +139,8 @@ public void onActivityCreated(Bundle savedInstanceState) { setUpInjections(); setUpFiles(savedInstanceState); + + setUpProgressDialog(); } private void setUpInjections() { @@ -148,6 +155,13 @@ private void setUpFiles(Bundle state) { setUpFilesContentRefreshing(); } + private void setUpProgressDialog() { + deleteProgressDialog = new ProgressDialog(getContext()); + deleteProgressDialog.setMessage(getString(R.string.message_delete_progress)); + deleteProgressDialog.setIndeterminate(true); + deleteProgressDialog.setCancelable(false); + } + private void setUpFilesMenu() { setHasOptionsMenu(true); } @@ -205,63 +219,113 @@ private void clearFileChoices() { getListView().requestLayout(); } - @RequiresApi(api = Build.VERSION_CODES.M) @Override public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.menu_share: - checkPermissions(); + if (Android.isPermissionRequired()) { + checkSharePermissions(actionMode); + } else { + startFileSharing(getCheckedFile()); + actionMode.finish(); + } + break; + case R.id.menu_delete: + deleteFile(getCheckedFile(), actionMode); break; - default: return false; } - actionMode.finish(); - return true; } + @RequiresApi(api = Build.VERSION_CODES.M) - private void checkPermissions(){ - int permissionCheck = checkSelfPermission(getActivity().getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE); + private void checkSharePermissions(ActionMode actionMode) { + String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE}; + if (EasyPermissions.hasPermissions(getContext(), perms)) { + startFileSharing(getCheckedFile()); + } else { + lastCheckedFileIndex = getListView().getCheckedItemPosition(); + EasyPermissions.requestPermissions(this, getString(R.string.share_permission), + SHARE_PERMISSIONS, perms); + } + actionMode.finish(); + } - if (!(permissionCheck == PackageManager.PERMISSION_GRANTED)) { + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CALLBACK_NUMBER); + // Forward results to EasyPermissions + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); + } - } else { - startFileSharing(getCheckedFile()); + @Override + public void onPermissionsGranted(int requestCode, List perms) { + if (requestCode == SHARE_PERMISSIONS) { + if (lastCheckedFileIndex != -1) { + startFileSharing(getFile(lastCheckedFileIndex)); + } } } @Override - public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { - switch (requestCode) { - case 100: { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Snackbar.make(getView(),getString(R.string.share_permission_granted),Snackbar.LENGTH_LONG).show(); - } else { - Snackbar.make(getView(),getString(R.string.share_permission_denied),Snackbar.LENGTH_LONG) - .setAction("Permissions", new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", getActivity().getPackageName(), null); - intent.setData(uri); - startActivity(intent); - } - }) - .show(); + public void onPermissionsDenied(int requestCode, List perms) { + if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) { + if (requestCode == SHARE_PERMISSIONS) { + showPermissionSnackBar(getString(R.string.share_permission_denied)); } - } } + + } + + private void showPermissionSnackBar(String message) { + Snackbar.make(getView(), message, Snackbar.LENGTH_LONG) + .setAction(R.string.menu_settings, new View.OnClickListener() { + @Override + public void onClick(View v) { + new AppSettingsDialog.Builder(ServerFilesFragment.this).build().show(); + } + }) + .show(); } private void startFileSharing(ServerFile file) { BusProvider.getBus().post(new ServerFileSharingEvent(getShare(), file)); } + private void deleteFile(final ServerFile file, final ActionMode actionMode) { + deleteFilePosition = getListView().getCheckedItemPosition(); + new AlertDialog.Builder(getContext()) + .setTitle(R.string.message_delete_file_title) + .setMessage(R.string.message_delete_file_body) + .setPositiveButton(R.string.button_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + deleteProgressDialog.show(); + serverClient.deleteFile(getShare(), file); + actionMode.finish(); + } + }) + .setNegativeButton(R.string.button_no, null) + .show(); + } + + @Subscribe + public void onFileDeleteEvent(ServerFileDeleteEvent fileDeleteEvent) { + deleteProgressDialog.dismiss(); + if (fileDeleteEvent.isDeleted()) { + if (!isMetadataAvailable()) { + getFilesAdapter().removeFile(deleteFilePosition); + } else { + getFilesMetadataAdapter().removeFile(deleteFilePosition); + } + } else { + Toast.makeText(getContext(), R.string.message_delete_file_error, Toast.LENGTH_SHORT).show(); + } + } + private ServerFile getCheckedFile() { return getFile(getListView().getCheckedItemPosition()); } @@ -376,14 +440,14 @@ private boolean areFilesAvailable() { } private void setUpFilesContent() { - if (serverClient.isConnected()){ - if (!isDirectoryAvailable()) { - serverClient.getFiles(getShare()); - } else { - serverClient.getFiles(getShare(), getDirectory()); - } - } - } + if (serverClient.isConnected()) { + if (!isDirectoryAvailable()) { + serverClient.getFiles(getShare()); + } else { + serverClient.getFiles(getShare(), getDirectory()); + } + } + } private boolean isDirectoryAvailable() { return getDirectory() != null; @@ -465,10 +529,10 @@ private void setUpFilesContentRefreshing() { SwipeRefreshLayout refreshLayout = getRefreshLayout(); refreshLayout.setColorSchemeResources( - android.R.color.holo_blue_light, - android.R.color.holo_orange_light, - android.R.color.holo_green_light, - android.R.color.holo_red_light); + android.R.color.holo_blue_light, + android.R.color.holo_orange_light, + android.R.color.holo_green_light, + android.R.color.holo_red_light); refreshLayout.setOnRefreshListener(this); } @@ -484,7 +548,7 @@ public void onItemClick(AdapterView filesListView, View fileView, int filePos collapseSearchView(); startFileOpening(getFile(filePosition)); - if(isDirectory(getFile(filePosition))){ + if (isDirectory(getFile(filePosition))) { setUpTitle(getFile(filePosition).getName()); } } @@ -495,8 +559,8 @@ private void startFileOpening(ServerFile file) { } private void setUpTitle(String title) { - ((ServerFilesActivity)getActivity()).getSupportActionBar().setTitle(title); - } + ((ServerFilesActivity) getActivity()).getSupportActionBar().setTitle(title); + } private List getFiles() { if (!isMetadataAvailable()) { @@ -533,13 +597,14 @@ private void setUpSearchView() { } private void setSearchCursor() { - final int textViewID = searchView.getContext().getResources().getIdentifier("android:id/search_src_text",null, null); + final int textViewID = searchView.getContext().getResources().getIdentifier("android:id/search_src_text", null, null); final AutoCompleteTextView searchTextView = (AutoCompleteTextView) searchView.findViewById(textViewID); try { Field mCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); mCursorDrawableRes.setAccessible(true); mCursorDrawableRes.set(searchTextView, R.drawable.white_cursor); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } private void setUpFilesContentSortIcon(MenuItem menuItem) { @@ -565,7 +630,6 @@ public boolean onOptionsItemSelected(MenuItem menuItem) { setUpFilesContentSortSwitched(); setUpFilesContentSortIcon(menuItem); return true; - default: return super.onOptionsItemSelected(menuItem); } @@ -613,8 +677,8 @@ public boolean onQueryTextChange(String s) { @Override public void isListEmpty(boolean empty) { - if(getView().findViewById(R.id.none_text)!=null) - getView().findViewById(R.id.none_text).setVisibility(empty?View.VISIBLE:View.GONE); + if (getView().findViewById(R.id.none_text) != null) + getView().findViewById(R.id.none_text).setVisibility(empty ? View.VISIBLE : View.GONE); } private void collapseSearchView() { @@ -624,6 +688,30 @@ private void collapseSearchView() { } } + public boolean checkForDuplicateFile(String fileName) { + List files; + + if (!isMetadataAvailable()) { + files = getFilesAdapter().getItems(); + } else { + files = getFilesAdapter().getItems(); + } + for (ServerFile serverFile : files) { + if (serverFile.getName().equals(fileName)) { + return true; + } + } + return false; + } + + public void refreshFileList() { + if (!isMetadataAvailable()) { + getFilesAdapter().notifyDataSetChanged(); + } else { + getFilesAdapter().notifyDataSetChanged(); + } + } + @Override public void onResume() { super.onResume(); @@ -673,16 +761,14 @@ private boolean areFilesLoaded() { } } - private static final class FileNameComparator implements Comparator - { + private static final class FileNameComparator implements Comparator { @Override public int compare(ServerFile firstFile, ServerFile secondFile) { return firstFile.getName().compareTo(secondFile.getName()); } } - private static final class FileModificationTimeComparator implements Comparator - { + private static final class FileModificationTimeComparator implements Comparator { @Override public int compare(ServerFile firstFile, ServerFile secondFile) { return -firstFile.getModificationTime().compareTo(secondFile.getModificationTime()); diff --git a/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java b/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java new file mode 100644 index 000000000..c1675346a --- /dev/null +++ b/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.fragment; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.BottomSheetDialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ListView; + +import org.amahi.anywhere.R; +import org.amahi.anywhere.adapter.UploadOptionsAdapter; +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.UploadClickEvent; +import org.amahi.anywhere.model.UploadOption; + +import java.util.ArrayList; + +/** + * Bottom sheet component for showing upload related options. + * Extends {@link android.support.design.widget.BottomSheetDialog} + */ +public class UploadBottomSheet extends BottomSheetDialogFragment implements AdapterView.OnItemClickListener { + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.upload_bottom_sheet, container); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setUpListView(view); + } + + private ArrayList getListItems() { + ArrayList uploadOptions = new ArrayList<>(); + + uploadOptions.add(new UploadOption(UploadOption.CAMERA, + getString(R.string.upload_camera), + R.drawable.ic_camera)); + + uploadOptions.add(new UploadOption(UploadOption.FILE, + getString(R.string.upload_photo), + R.drawable.ic_cloud_upload)); + + return uploadOptions; + } + + private void setUpListView(View view) { + UploadOptionsAdapter adapter = new UploadOptionsAdapter(getContext(), getListItems()); + ListView listView = (ListView) view.findViewById(R.id.upload_options_list); + assert listView != null; + listView.setAdapter(adapter); + listView.setOnItemClickListener(this); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + UploadOption uploadOption = getListItems().get(position); + BusProvider.getBus().post(new UploadClickEvent(uploadOption.getType())); + dismiss(); + } +} diff --git a/src/main/java/org/amahi/anywhere/model/UploadOption.java b/src/main/java/org/amahi/anywhere/model/UploadOption.java new file mode 100644 index 000000000..8a4de4306 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/model/UploadOption.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.model; + +import android.support.annotation.IntDef; + +import org.amahi.anywhere.fragment.UploadBottomSheet; + +/** + * Upload option model for display in {@link UploadBottomSheet} + */ +public class UploadOption { + public static final int CAMERA = 1; + public static final int FILE = 2; + + @IntDef({CAMERA, FILE}) + public @interface Types { + } + + @Types + private int type; + private String name; + private int icon; + + public UploadOption(@Types int type, String name, int icon) { + this.name = name; + this.icon = icon; + this.type = type; + } + + @Types + public int getType() { + return type; + } + + public String getName() { + return name; + } + + public int getIcon() { + return icon; + } +} diff --git a/src/main/java/org/amahi/anywhere/server/api/ServerApi.java b/src/main/java/org/amahi/anywhere/server/api/ServerApi.java index de1bba278..ecc2fa26f 100644 --- a/src/main/java/org/amahi/anywhere/server/api/ServerApi.java +++ b/src/main/java/org/amahi/anywhere/server/api/ServerApi.java @@ -26,33 +26,52 @@ import java.util.List; +import okhttp3.MultipartBody; +import okhttp3.ResponseBody; import retrofit2.Call; +import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; import retrofit2.http.Query; /** * Server API declaration. */ -public interface ServerApi -{ - @GET("/shares") - Call> getShares( - @Header("Session") String session); - - @GET("/files") - Call> getFiles( - @Header("Session") String session, - @Query("s") String share, - @Query("p") String path); - - @GET("/md") - Call getFileMetadata( - @Header("Session") String session, - @Query("f") String fileName, - @Query("h") String hint); - - @GET("/apps") - Call> getApps( - @Header("Session") String session); +public interface ServerApi { + @GET("/shares") + Call> getShares( + @Header("Session") String session); + + @GET("/files") + Call> getFiles( + @Header("Session") String session, + @Query("s") String share, + @Query("p") String path); + + @DELETE("/files") + Call deleteFile( + @Header("Session") String session, + @Query("s") String share, + @Query("p") String path); + + @Multipart + @POST("/files") + Call uploadFile( + @Header("Session") String session, + @Query("s") String share, + @Query("p") String path, + @Part MultipartBody.Part file); + + @GET("/md") + Call getFileMetadata( + @Header("Session") String session, + @Query("f") String fileName, + @Query("h") String hint); + + @GET("/apps") + Call> getApps( + @Header("Session") String session); } diff --git a/src/main/java/org/amahi/anywhere/server/client/ServerClient.java b/src/main/java/org/amahi/anywhere/server/client/ServerClient.java index 740baaa95..d0d1d95b8 100644 --- a/src/main/java/org/amahi/anywhere/server/client/ServerClient.java +++ b/src/main/java/org/amahi/anywhere/server/client/ServerClient.java @@ -40,17 +40,21 @@ import org.amahi.anywhere.server.model.ServerRoute; import org.amahi.anywhere.server.model.ServerShare; import org.amahi.anywhere.server.response.ServerAppsResponse; +import org.amahi.anywhere.server.response.ServerFileDeleteResponse; +import org.amahi.anywhere.server.response.ServerFileUploadResponse; import org.amahi.anywhere.server.response.ServerFilesResponse; import org.amahi.anywhere.server.response.ServerRouteResponse; import org.amahi.anywhere.server.response.ServerSharesResponse; import org.amahi.anywhere.task.ServerConnectionDetectingTask; +import org.amahi.anywhere.util.ProgressRequestBody; import org.amahi.anywhere.util.Time; -import java.io.IOException; +import java.io.File; import javax.inject.Inject; import javax.inject.Singleton; +import okhttp3.MultipartBody; import retrofit2.Callback; @@ -59,8 +63,7 @@ * {@link org.amahi.anywhere.server.api.ServerApi}. Reacts to network connection changes as well. */ @Singleton -public class ServerClient -{ +public class ServerClient { private final ApiAdapter apiAdapter; private final ProxyApi proxyApi; private ServerApi serverApi; @@ -179,18 +182,18 @@ public void connectAuto() { public void connectLocal() { this.serverConnection = ApiConnection.LOCAL; - if (!isServerRouteLoaded()) { - return; - } + if (!isServerRouteLoaded()) { + return; + } this.serverAddress = serverRoute.getLocalAddress(); this.serverApi = buildServerApi(); } public void connectRemote() { this.serverConnection = ApiConnection.REMOTE; - if (!isServerRouteLoaded()) { - return; - } + if (!isServerRouteLoaded()) { + return; + } this.serverAddress = serverRoute.getRemoteAddress(); this.serverApi = buildServerApi(); } @@ -215,21 +218,42 @@ public void getFiles(ServerShare share, ServerFile directory) { serverApi.getFiles(server.getSession(), share.getName(), directory.getPath()).enqueue(new ServerFilesResponse(directory, share)); } + public void deleteFile(ServerShare share, ServerFile serverFile) { + serverApi.deleteFile(server.getSession(), share.getName(), serverFile.getPath()) + .enqueue(new ServerFileDeleteResponse()); + } + + public void uploadFile(File file, ServerShare share) { + this.uploadFile(file, share, null); + } + + public void uploadFile(File file, ServerShare share, ServerFile directory) { + MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", + file.getName(), + new ProgressRequestBody(file)); + String path = "/"; + if (directory != null) + path = directory.getPath(); + + serverApi.uploadFile(server.getSession(), share.getName(), path, filePart) + .enqueue(new ServerFileUploadResponse()); + } + public Uri getFileUri(ServerShare share, ServerFile file) { return Uri.parse(serverAddress) - .buildUpon() - .path("files") - .appendQueryParameter("s", share.getName()) - .appendQueryParameter("p", file.getPath()) - .appendQueryParameter("mtime", Time.getEpochTimeString(file.getModificationTime())) - .appendQueryParameter("session", server.getSession()) - .build(); + .buildUpon() + .path("files") + .appendQueryParameter("s", share.getName()) + .appendQueryParameter("p", file.getPath()) + .appendQueryParameter("mtime", Time.getEpochTimeString(file.getModificationTime())) + .appendQueryParameter("session", server.getSession()) + .build(); } public void getFileMetadata(ServerShare share, ServerFile file, Callback callback) { - if ((server == null) || (share == null) || (file == null)){ - return; - } + if ((server == null) || (share == null) || (file == null)) { + return; + } serverApi.getFileMetadata(server.getSession(), file.getName(), share.getTag()).enqueue(callback); } diff --git a/src/main/java/org/amahi/anywhere/server/response/ServerFileDeleteResponse.java b/src/main/java/org/amahi/anywhere/server/response/ServerFileDeleteResponse.java new file mode 100644 index 000000000..a1f8880e5 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/server/response/ServerFileDeleteResponse.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.server.response; + +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.ServerFileDeleteEvent; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.HttpException; +import retrofit2.Response; + + +/** + * File delete response proxy. Consumes API callback and posts it via {@link com.squareup.otto.Bus} + * as {@link org.amahi.anywhere.bus.BusEvent}. + */ +public class ServerFileDeleteResponse implements Callback { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + BusProvider.getBus().post(new ServerFileDeleteEvent(true)); + } else + this.onFailure(call, new HttpException(response)); + } + + @Override + public void onFailure(Call call, Throwable t) { + BusProvider.getBus().post(new ServerFileDeleteEvent(false)); + } +} diff --git a/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java b/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java new file mode 100644 index 000000000..e58e5ad7e --- /dev/null +++ b/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.server.response; + +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.ServerFileUploadCompleteEvent; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.HttpException; +import retrofit2.Response; + + +/** + * File upload response proxy. Consumes API callback and posts it via {@link com.squareup.otto.Bus} + * as {@link org.amahi.anywhere.bus.BusEvent}. + */ +public class ServerFileUploadResponse implements Callback { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + BusProvider.getBus().post(new ServerFileUploadCompleteEvent(true)); + } else + this.onFailure(call, new HttpException(response)); + } + + @Override + public void onFailure(Call call, Throwable t) { + BusProvider.getBus().post(new ServerFileUploadCompleteEvent(false)); + } +} diff --git a/src/main/java/org/amahi/anywhere/util/Android.java b/src/main/java/org/amahi/anywhere/util/Android.java index 9ef6abec3..d684a15c1 100644 --- a/src/main/java/org/amahi/anywhere/util/Android.java +++ b/src/main/java/org/amahi/anywhere/util/Android.java @@ -38,6 +38,10 @@ public static boolean isTablet(Context context) { return context.getResources().getBoolean(R.bool.tablet); } + public static boolean isPermissionRequired() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + public static String getVersion() { return Build.VERSION.RELEASE; } diff --git a/src/main/java/org/amahi/anywhere/util/Intents.java b/src/main/java/org/amahi/anywhere/util/Intents.java index 37d75d088..a0be37013 100644 --- a/src/main/java/org/amahi/anywhere/util/Intents.java +++ b/src/main/java/org/amahi/anywhere/util/Intents.java @@ -25,8 +25,11 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Build; import android.os.Parcelable; +import android.provider.MediaStore; +import org.amahi.anywhere.R; import org.amahi.anywhere.activity.NativeVideoActivity; import org.amahi.anywhere.activity.ServerAppActivity; import org.amahi.anywhere.activity.ServerFileAudioActivity; @@ -210,5 +213,20 @@ public Intent buildGooglePlaySearchIntent(String search) { return intent; } + + public Intent buildMediaPickerIntent () { + Intent intent = new Intent(Intent.ACTION_PICK, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + intent.setType("image/* video/*"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/*", "video/*"}); + } + intent = Intent.createChooser(intent, context.getString(R.string.message_media_upload)); + return intent; + } + + public Intent buildCameraIntent() { + return new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + } } } diff --git a/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java b/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java new file mode 100644 index 000000000..24eb0f385 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.util; + +import android.os.Handler; +import android.os.Looper; + +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.ServerFileUploadProgressEvent; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; + +/** + * Extension of RequestBody {@link okhttp3.RequestBody} to provide progress callbacks + * for file upload. + */ +public class ProgressRequestBody extends RequestBody { + private File mFile; + + private static final int DEFAULT_BUFFER_SIZE = 2048; + + public ProgressRequestBody(final File file) { + mFile = file; + } + + @Override + public MediaType contentType() { + // Only for uploading images + return MediaType.parse("image/*"); + } + + @Override + public long contentLength() throws IOException { + return mFile.length(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + long fileLength = mFile.length(); + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + FileInputStream in = new FileInputStream(mFile); + long uploaded = 0; + + //noinspection TryFinallyCanBeTryWithResources + try { + int read; + Handler handler = new Handler(Looper.getMainLooper()); + while ((read = in.read(buffer)) != -1) { + + uploaded += read; + sink.write(buffer, 0, read); + + // update progress on UI thread + handler.post(new ProgressUpdater(uploaded, fileLength)); + } + } finally { + in.close(); + } + } + + private class ProgressUpdater implements Runnable { + private long mUploaded; + private long mTotal; + + ProgressUpdater(long uploaded, long total) { + mUploaded = uploaded; + mTotal = total; + } + + @Override + public void run() { + BusProvider.getBus().post(new ServerFileUploadProgressEvent((int) (100 * mUploaded / mTotal))); + } + } +} diff --git a/src/main/res/drawable/ic_add.xml b/src/main/res/drawable/ic_add.xml new file mode 100644 index 000000000..6b6146ecc --- /dev/null +++ b/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/main/res/drawable/ic_camera.xml b/src/main/res/drawable/ic_camera.xml new file mode 100644 index 000000000..c872f1670 --- /dev/null +++ b/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,12 @@ + + + + diff --git a/src/main/res/drawable/ic_cloud_upload.xml b/src/main/res/drawable/ic_cloud_upload.xml new file mode 100644 index 000000000..086281669 --- /dev/null +++ b/src/main/res/drawable/ic_cloud_upload.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/main/res/drawable/ic_delete.xml b/src/main/res/drawable/ic_delete.xml new file mode 100644 index 000000000..650c02224 --- /dev/null +++ b/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_server_files.xml b/src/main/res/layout/activity_server_files.xml index d5b7fe045..ccaa96d2e 100644 --- a/src/main/res/layout/activity_server_files.xml +++ b/src/main/res/layout/activity_server_files.xml @@ -19,7 +19,23 @@ ~ along with Amahi. If not, see . --> - \ No newline at end of file + android:layout_height="match_parent"> + + + + + diff --git a/src/main/res/layout/upload_bottom_sheet.xml b/src/main/res/layout/upload_bottom_sheet.xml new file mode 100644 index 000000000..1e597f6da --- /dev/null +++ b/src/main/res/layout/upload_bottom_sheet.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/upload_list_item.xml b/src/main/res/layout/upload_list_item.xml new file mode 100644 index 000000000..8b498dc35 --- /dev/null +++ b/src/main/res/layout/upload_list_item.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/res/menu/action_bar_server_files.xml b/src/main/res/menu/action_bar_server_files.xml index b56c31734..21a4d6811 100644 --- a/src/main/res/menu/action_bar_server_files.xml +++ b/src/main/res/menu/action_bar_server_files.xml @@ -18,7 +18,7 @@ --> + xmlns:app="http://schemas.android.com/apk/res-auto"> + app:showAsAction="collapseActionView|always"/> + app:showAsAction="always"/> \ No newline at end of file diff --git a/src/main/res/menu/action_mode_server_files.xml b/src/main/res/menu/action_mode_server_files.xml index 83c518619..65ba87fa3 100644 --- a/src/main/res/menu/action_mode_server_files.xml +++ b/src/main/res/menu/action_mode_server_files.xml @@ -17,13 +17,20 @@ ~ along with Amahi. If not, see . --> - + app:showAsAction="always"/> + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 38b65425d..536f6810c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -24,9 +24,13 @@ Apps Settings Shares + Upload Search Google Play Sign in + Yes + No + Retry Password Username @@ -37,8 +41,10 @@ Open navigation Settings Share + Delete Sort Search + Upload File No Apps No Files @@ -61,6 +67,17 @@ Create some and try again Connection Error + Confirm File Delete? + This action cannot be undone. + Deleting + Some error occurred while deleting file + Select Media Files to Upload + Uploading your file + File uploaded successfully + There was some error in uploading your file + Overwrite existing file? + The file %1$s already exists.\nAre you sure you want to replace the existing file? + Check out my Amahi home server! I use the Amahi Home Server for storing, backing up and streaming all my files.\n\nCheck it out!\n\nhttps://www.amahi.org/ Amahi for Android @@ -68,14 +85,22 @@ No Handler Application Found Logged out successfully Share Using - Storage permission has been enabled please reshare to download. - You have denied the permissions for sharing. Enable permissions to continue. + Storage permission is required to download the file + You have denied permissions for storage. Enable to continue + File read permission is required to access files on your device + You have denied permissions for reading files. Enable to continue + Camera access is required to capture photos and videos + Could not access the camera. Permissions denied + Previous Next Play Pause + Use Camera + Upload Photos / Videos + Amahi TV INTENT_FILE diff --git a/src/main/res/xml/file_paths.xml b/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..9296c4cd0 --- /dev/null +++ b/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From a26db6bc0cc28406673617be803db27ee49502ac Mon Sep 17 00:00:00 2001 From: Chirag Maheshwari Date: Sun, 16 Jul 2017 17:15:14 +0530 Subject: [PATCH 2/8] fix: forwarding to the desired action after grating permissions --- .../org/amahi/anywhere/activity/ServerFilesActivity.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java index 533d4e203..eab8f3f59 100644 --- a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java +++ b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; +import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; @@ -284,6 +285,14 @@ private void showGooglePlaySearchFragment(ServerFile file) { fragment.show(getFragmentManager(), GooglePlaySearchFragment.TAG); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + // Forward results to EasyPermissions + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); + } + @Override public void onPermissionsGranted(int requestCode, List perms) { if (requestCode == FILE_UPLOAD_PERMISSION) { From 956f01ce444d8e1da7e9042ab3db046ec01d3ad2 Mon Sep 17 00:00:00 2001 From: Chirag Maheshwari Date: Sun, 16 Jul 2017 19:23:11 +0530 Subject: [PATCH 3/8] feature to add custom hda servers from json --- src/main/assets/.gitignore | 1 + .../anywhere/fragment/NavigationFragment.java | 4 +- .../anywhere/server/client/AmahiClient.java | 6 ++- .../anywhere/server/client/ServerClient.java | 23 +++++++++- .../amahi/anywhere/server/model/Server.java | 25 +++++++++-- .../anywhere/server/model/ServerRoute.java | 8 ++++ .../server/response/ServersResponse.java | 42 +++++++++++++++++-- .../java/org/amahi/anywhere/util/Android.java | 17 ++++++++ 8 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 src/main/assets/.gitignore diff --git a/src/main/assets/.gitignore b/src/main/assets/.gitignore new file mode 100644 index 000000000..fd77dc41a --- /dev/null +++ b/src/main/assets/.gitignore @@ -0,0 +1 @@ +customServers.json \ No newline at end of file diff --git a/src/main/java/org/amahi/anywhere/fragment/NavigationFragment.java b/src/main/java/org/amahi/anywhere/fragment/NavigationFragment.java index dfa73c1a4..51db282e9 100644 --- a/src/main/java/org/amahi/anywhere/fragment/NavigationFragment.java +++ b/src/main/java/org/amahi/anywhere/fragment/NavigationFragment.java @@ -318,7 +318,7 @@ private void setUpServers(String authenticationToken) { } private void setUpServersContent(String authenticationToken) { - amahiClient.getServers(authenticationToken); + amahiClient.getServers(getContext(), authenticationToken); } @Subscribe @@ -414,7 +414,7 @@ private void setUpServerConnection(Server server) { setUpServerConnection(); setUpServerNavigation(); } else { - serverClient.connect(server); + serverClient.connect(getContext(), server); } } diff --git a/src/main/java/org/amahi/anywhere/server/client/AmahiClient.java b/src/main/java/org/amahi/anywhere/server/client/AmahiClient.java index 543269379..c61ece367 100644 --- a/src/main/java/org/amahi/anywhere/server/client/AmahiClient.java +++ b/src/main/java/org/amahi/anywhere/server/client/AmahiClient.java @@ -19,6 +19,8 @@ package org.amahi.anywhere.server.client; +import android.content.Context; + import org.amahi.anywhere.server.Api; import org.amahi.anywhere.server.ApiAdapter; import org.amahi.anywhere.server.api.AmahiApi; @@ -49,7 +51,7 @@ public void authenticate(String username, String password) { api.authenticate(Api.getClientId(), Api.getClientSecret(), username, password).enqueue(new AuthenticationResponse()); } - public void getServers(String authenticationToken) { - api.getServers(authenticationToken).enqueue(new ServersResponse()); + public void getServers(Context context, String authenticationToken) { + api.getServers(authenticationToken).enqueue(new ServersResponse(context)); } } diff --git a/src/main/java/org/amahi/anywhere/server/client/ServerClient.java b/src/main/java/org/amahi/anywhere/server/client/ServerClient.java index d0d1d95b8..b6c3351d0 100644 --- a/src/main/java/org/amahi/anywhere/server/client/ServerClient.java +++ b/src/main/java/org/amahi/anywhere/server/client/ServerClient.java @@ -19,6 +19,7 @@ package org.amahi.anywhere.server.client; +import android.content.Context; import android.net.Uri; import com.squareup.otto.Subscribe; @@ -48,6 +49,9 @@ import org.amahi.anywhere.task.ServerConnectionDetectingTask; import org.amahi.anywhere.util.ProgressRequestBody; import org.amahi.anywhere.util.Time; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import java.io.File; @@ -57,6 +61,8 @@ import okhttp3.MultipartBody; import retrofit2.Callback; +import static org.amahi.anywhere.util.Android.loadServersFromAsset; + /** * Server API implementation. Wraps {@link org.amahi.anywhere.server.api.ProxyApi} and @@ -151,10 +157,23 @@ public boolean isConnectedLocal() { return serverAddress.equals(serverRoute.getLocalAddress()); } - public void connect(Server server) { + public void connect(Context context, Server server) { this.server = server; - startServerConnection(); + if (server.isDebug()) { + try { + ServerRoute serverRoute = new ServerRoute(); + JSONArray jsonArray = new JSONArray(loadServersFromAsset(context)); + JSONObject jsonObject = jsonArray.getJSONObject(server.getIndex()); + serverRoute.setLocalAddress(jsonObject.getString("local_address")); + serverRoute.setRemoteAddress(jsonObject.getString("remote_address")); + BusProvider.getBus().post(new ServerRouteLoadedEvent(serverRoute)); + } catch (JSONException e) { + e.printStackTrace(); + } + } else { + startServerConnection(); + } } private void startServerConnection() { diff --git a/src/main/java/org/amahi/anywhere/server/model/Server.java b/src/main/java/org/amahi/anywhere/server/model/Server.java index dabd2b30d..42df9fe0c 100644 --- a/src/main/java/org/amahi/anywhere/server/model/Server.java +++ b/src/main/java/org/amahi/anywhere/server/model/Server.java @@ -38,6 +38,10 @@ public class Server implements Parcelable @SerializedName("active") private boolean active; + private boolean debug = false; + + private int index; + public String getName() { return name; } @@ -46,12 +50,19 @@ public String getSession() { return session; } + public int getIndex() { + return index; + } + public boolean isActive() { return active; } - public static final Creator CREATOR = new Creator() - { + public boolean isDebug() { + return debug; + } + + public static final Creator CREATOR = new Creator() { @Override public Server createFromParcel(Parcel parcel) { return new Server(parcel); @@ -63,7 +74,15 @@ public Server[] newArray(int size) { } }; - private Server(Parcel parcel) { + public Server(int index, String name, String session) { + this.index = index; + this.name = name; + this.session = session; + this.active = true; + this.debug = true; + } + + public Server(Parcel parcel) { this.name = parcel.readString(); this.session = parcel.readString(); this.active = Boolean.valueOf(parcel.readString()); diff --git a/src/main/java/org/amahi/anywhere/server/model/ServerRoute.java b/src/main/java/org/amahi/anywhere/server/model/ServerRoute.java index f463f3ebf..80e9106ac 100644 --- a/src/main/java/org/amahi/anywhere/server/model/ServerRoute.java +++ b/src/main/java/org/amahi/anywhere/server/model/ServerRoute.java @@ -39,4 +39,12 @@ public String getLocalAddress() { public String getRemoteAddress() { return remoteAddress; } + + public void setLocalAddress(String localAddress) { + this.localAddress = localAddress; + } + + public void setRemoteAddress(String remoteAddress) { + this.remoteAddress = remoteAddress; + } } diff --git a/src/main/java/org/amahi/anywhere/server/response/ServersResponse.java b/src/main/java/org/amahi/anywhere/server/response/ServersResponse.java index e136cedbe..827cdc488 100644 --- a/src/main/java/org/amahi/anywhere/server/response/ServersResponse.java +++ b/src/main/java/org/amahi/anywhere/server/response/ServersResponse.java @@ -19,11 +19,18 @@ package org.amahi.anywhere.server.response; +import android.content.Context; + +import org.amahi.anywhere.BuildConfig; import org.amahi.anywhere.bus.BusProvider; import org.amahi.anywhere.bus.ServersLoadFailedEvent; import org.amahi.anywhere.bus.ServersLoadedEvent; import org.amahi.anywhere.server.model.Server; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.ArrayList; import java.util.List; import retrofit2.Call; @@ -31,17 +38,46 @@ import retrofit2.HttpException; import retrofit2.Response; +import static org.amahi.anywhere.util.Android.loadServersFromAsset; + /** * Servers response proxy. Consumes API callback and posts it via {@link com.squareup.otto.Bus} * as {@link org.amahi.anywhere.bus.BusEvent}. */ public class ServersResponse implements Callback> { + private Context context; + + public ServersResponse(Context context) { + this.context = context; + } + + private List getLocalServers() { + List servers = new ArrayList<>(); + try { + JSONArray jsonArray = new JSONArray(loadServersFromAsset(context)); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + Server server = new Server(i, jsonObject.getString("name"), + jsonObject.getString("session_token")); + servers.add(server); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return servers; + } + + @Override public void onResponse(Call> call, Response> response) { - if (response.isSuccessful()) - BusProvider.getBus().post(new ServersLoadedEvent(response.body())); - else + if (response.isSuccessful()) { + List servers = response.body(); + if (BuildConfig.DEBUG) { + servers.addAll(getLocalServers()); + } + BusProvider.getBus().post(new ServersLoadedEvent(servers)); + } else this.onFailure(call, new HttpException(response)); } diff --git a/src/main/java/org/amahi/anywhere/util/Android.java b/src/main/java/org/amahi/anywhere/util/Android.java index d684a15c1..42b6ce59e 100644 --- a/src/main/java/org/amahi/anywhere/util/Android.java +++ b/src/main/java/org/amahi/anywhere/util/Android.java @@ -27,6 +27,9 @@ import org.amahi.anywhere.BuildConfig; import org.amahi.anywhere.R; +import java.io.IOException; +import java.io.InputStream; + /** * Android properties accessor. */ @@ -84,4 +87,18 @@ private static DisplayMetrics getDeviceScreenMetrics(Context context) { return screenMetrics; } + + public static String loadServersFromAsset(Context context) { + String json = "[]"; + try { + InputStream is = context.getAssets().open("customServers.json"); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + json = new String(buffer, "UTF-8"); + } catch (IOException ignored) { + } + return json; + } } From 2a67b11c2ad4926189ac8bebb3dd6fa59f85ac8f Mon Sep 17 00:00:00 2001 From: Carlos Puchol Date: Sun, 16 Jul 2017 12:49:17 -0700 Subject: [PATCH 4/8] add debug file --- DEBUG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 DEBUG.md diff --git a/DEBUG.md b/DEBUG.md new file mode 100644 index 000000000..365e16dbb --- /dev/null +++ b/DEBUG.md @@ -0,0 +1,24 @@ +# Debugging + +Sometimes you may need to debug with some special purpose server. To do that, add a file like this + + src/main/assets/customServers.json + +with details of the custom server(s) you need, like this: + + ``` + [ + { + "name": "Test Server 1", + "session_token": "12345678901234567", + "local_address": "http://192.168.0.11:4563", + "remote_address": "http://192.168.12.22:4563" + }, + { + "name": "Test Server 2", + "session_token": "12345678901234567", + "local_address": "http://192.168.0.11:4563", + "remote_address": "http://192.168.12.22:4563" + } + ] + ``` From e51eebfe984d1c8ac88f1a003d4c3dc9f0731f04 Mon Sep 17 00:00:00 2001 From: Chirag Maheshwari Date: Sun, 30 Jul 2017 21:47:32 +0530 Subject: [PATCH 5/8] resolve crash on uploading files from movies share --- .../java/org/amahi/anywhere/fragment/ServerFilesFragment.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java index dda0161ce..2b5e944d7 100644 --- a/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java +++ b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java @@ -44,6 +44,7 @@ import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; +import android.widget.BaseAdapter; import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.TextView; @@ -694,7 +695,7 @@ public boolean checkForDuplicateFile(String fileName) { if (!isMetadataAvailable()) { files = getFilesAdapter().getItems(); } else { - files = getFilesAdapter().getItems(); + files = getFilesMetadataAdapter().getItems(); } for (ServerFile serverFile : files) { if (serverFile.getName().equals(fileName)) { From 9bd358f3b899c8040133182b1c89eee4ad17a59d Mon Sep 17 00:00:00 2001 From: Kumar Shashwat Date: Tue, 15 Aug 2017 01:10:34 +0530 Subject: [PATCH 6/8] Fixed the landscape bottomSheet hidden by default bug. (#257) * Fixed the landscape bottomSheet hidden by default bug. * Adds android-19 to the travis.yml. --- .travis.yml | 1 + .../anywhere/fragment/UploadBottomSheet.java | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c197f6922..e4c9ce70a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ android: - platform-tools - build-tools-25.0.2 - android-25 + - android-19 - extra-android-support - extra-android-m2repository - extra-google-m2repository diff --git a/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java b/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java index c1675346a..053d04f3c 100644 --- a/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java +++ b/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java @@ -21,11 +21,15 @@ import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.design.widget.BottomSheetBehavior; +import android.support.design.widget.BottomSheetDialog; import android.support.design.widget.BottomSheetDialogFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.widget.AdapterView; +import android.widget.FrameLayout; import android.widget.ListView; import org.amahi.anywhere.R; @@ -46,7 +50,19 @@ public class UploadBottomSheet extends BottomSheetDialogFragment implements Adap @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.upload_bottom_sheet, container); + View rootView = inflater.inflate(R.layout.upload_bottom_sheet, container); + rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + BottomSheetDialog bottomSheetDialog = (BottomSheetDialog)getDialog(); + FrameLayout bottomSheet = (FrameLayout)bottomSheetDialog.findViewById(android.support.design.R.id.design_bottom_sheet); + assert bottomSheet != null; + BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + behavior.setPeekHeight(0); + } + }); + return rootView; } @Override From 4eb73602d28358c3c8b229df7a9dad3642ca80ee Mon Sep 17 00:00:00 2001 From: Chirag Maheshwari Date: Tue, 15 Aug 2017 04:18:54 +0530 Subject: [PATCH 7/8] Auto upload feature (#253) * adds auto upload settings to preference screen * adds NEW_PICTURE broadcast receiver * adds upload service * adds upload settings preference fragment * adds hda and share list fetching to upload settings * adds sqlite db for storing image paths * adds custom upload queue * implements upload manager and server connection in background service * fix the UI stuck on file uploads * fix multiple files uploading at the same time * code refactoring * adds upload only on wifi preference option * adds api-19 to travis.yml * adds Job for tracking changes in images content uri * adds job scheduling to the application class * adds net connnectivity job * adds permission checker to auto upload settings --- src/main/AndroidManifest.xml | 19 + .../org/amahi/anywhere/AmahiApplication.java | 28 +- .../java/org/amahi/anywhere/AmahiModule.java | 8 +- .../activity/ServerFilesActivity.java | 18 +- .../anywhere/activity/SettingsActivity.java | 37 +- .../bus/ServerFileUploadCompleteEvent.java | 20 +- .../bus/ServerFileUploadProgressEvent.java | 20 +- .../bus/UploadSettingsOpeningEvent.java | 23 + .../org/amahi/anywhere/db/UploadQueueDb.java | 44 ++ .../anywhere/db/UploadQueueDbHelper.java | 94 ++++ .../fragment/ServerFilesFragment.java | 8 - .../anywhere/fragment/SettingsFragment.java | 74 ++-- .../fragment/UploadSettingsFragment.java | 419 ++++++++++++++++++ .../anywhere/job/NetConnectivityJob.java | 91 ++++ .../amahi/anywhere/job/PhotosContentJob.java | 128 ++++++ .../org/amahi/anywhere/model/UploadFile.java | 38 ++ .../anywhere/receiver/CameraReceiver.java | 43 ++ .../anywhere/receiver/NetworkReceiver.java | 31 +- .../anywhere/server/client/ServerClient.java | 45 +- .../amahi/anywhere/server/model/Server.java | 4 + .../response/ServerFileUploadResponse.java | 30 +- .../amahi/anywhere/service/UploadService.java | 313 +++++++++++++ .../java/org/amahi/anywhere/util/Intents.java | 14 + .../org/amahi/anywhere/util/NetworkUtils.java | 64 +++ .../anywhere/util/ProgressRequestBody.java | 121 ++--- .../amahi/anywhere/util/UploadManager.java | 158 +++++++ src/main/res/values/preferences.xml | 15 + src/main/res/values/strings.xml | 4 + src/main/res/xml/settings.xml | 16 +- src/main/res/xml/upload_settings.xml | 49 ++ 30 files changed, 1814 insertions(+), 162 deletions(-) create mode 100644 src/main/java/org/amahi/anywhere/bus/UploadSettingsOpeningEvent.java create mode 100644 src/main/java/org/amahi/anywhere/db/UploadQueueDb.java create mode 100644 src/main/java/org/amahi/anywhere/db/UploadQueueDbHelper.java create mode 100644 src/main/java/org/amahi/anywhere/fragment/UploadSettingsFragment.java create mode 100644 src/main/java/org/amahi/anywhere/job/NetConnectivityJob.java create mode 100644 src/main/java/org/amahi/anywhere/job/PhotosContentJob.java create mode 100644 src/main/java/org/amahi/anywhere/model/UploadFile.java create mode 100644 src/main/java/org/amahi/anywhere/receiver/CameraReceiver.java create mode 100644 src/main/java/org/amahi/anywhere/service/UploadService.java create mode 100644 src/main/java/org/amahi/anywhere/util/NetworkUtils.java create mode 100644 src/main/java/org/amahi/anywhere/util/UploadManager.java create mode 100644 src/main/res/xml/upload_settings.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index af0f9124c..09885322c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -147,6 +147,7 @@ + @@ -166,6 +167,24 @@ + + + + + + + + + + + + diff --git a/src/main/java/org/amahi/anywhere/AmahiApplication.java b/src/main/java/org/amahi/anywhere/AmahiApplication.java index 6ce1a3243..2b86b62db 100644 --- a/src/main/java/org/amahi/anywhere/AmahiApplication.java +++ b/src/main/java/org/amahi/anywhere/AmahiApplication.java @@ -21,10 +21,15 @@ import android.app.Application; import android.content.Context; +import android.os.Build; import android.os.StrictMode; +import android.support.annotation.RequiresApi; import com.crashlytics.android.Crashlytics; +import org.amahi.anywhere.job.NetConnectivityJob; +import org.amahi.anywhere.job.PhotosContentJob; + import dagger.ObjectGraph; import io.fabric.sdk.android.Fabric; import timber.log.Timber; @@ -49,9 +54,13 @@ public void onCreate() { setUpDetecting(); setUpInjections(); - } - private void setUpLogging() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + setUpJobs(); + } + } + + private void setUpLogging() { if (isDebugging()) { Timber.plant(new Timber.DebugTree()); } @@ -80,4 +89,19 @@ private void setUpInjections() { public void inject(Object injectionsConsumer) { injector.inject(injectionsConsumer); } + + public static class JobIds { + public static final int PHOTOS_CONTENT_JOB = 125; + public static final int NET_CONNECTIVITY_JOB = 126; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private void setUpJobs() { + if (!PhotosContentJob.isScheduled(this)) { + PhotosContentJob.scheduleJob(this); + } + if (!NetConnectivityJob.isScheduled(this)) { + NetConnectivityJob.scheduleJob(this); + } + } } diff --git a/src/main/java/org/amahi/anywhere/AmahiModule.java b/src/main/java/org/amahi/anywhere/AmahiModule.java index 90d40f05b..2d9b94820 100644 --- a/src/main/java/org/amahi/anywhere/AmahiModule.java +++ b/src/main/java/org/amahi/anywhere/AmahiModule.java @@ -38,8 +38,10 @@ import org.amahi.anywhere.fragment.ServerFilesFragment; import org.amahi.anywhere.fragment.ServerSharesFragment; import org.amahi.anywhere.fragment.SettingsFragment; +import org.amahi.anywhere.fragment.UploadSettingsFragment; import org.amahi.anywhere.server.ApiModule; import org.amahi.anywhere.service.AudioService; +import org.amahi.anywhere.service.UploadService; import org.amahi.anywhere.service.VideoService; import org.amahi.anywhere.tv.activity.TVWebViewActivity; import org.amahi.anywhere.tv.activity.TvPlaybackOverlayActivity; @@ -47,6 +49,7 @@ import org.amahi.anywhere.tv.fragment.ServerFileTvFragment; import org.amahi.anywhere.tv.fragment.ServerSelectFragment; import org.amahi.anywhere.tv.fragment.TvPlaybackOverlayFragment; +import org.amahi.anywhere.util.UploadManager; import javax.inject.Singleton; @@ -78,13 +81,16 @@ ServerFileImageFragment.class, ServerFileDownloadingFragment.class, SettingsFragment.class, + UploadSettingsFragment.class, AudioService.class, VideoService.class, MainTVFragment.class, TVWebViewActivity.class, ServerFileTvFragment.class, TvPlaybackOverlayFragment.class, - TvPlaybackOverlayActivity.class + TvPlaybackOverlayActivity.class, + UploadService.class, + UploadManager.class } ) class AmahiModule { diff --git a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java index eab8f3f59..2fa98278d 100644 --- a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java +++ b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java @@ -408,13 +408,15 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { String filePath = querySelectedImagePath(selectedImageUri); if (filePath != null) { File file = new File(filePath); - ServerFilesFragment fragment = (ServerFilesFragment) - getSupportFragmentManager() - .findFragmentById(R.id.container_files); - if (fragment.checkForDuplicateFile(file.getName())) { - showDuplicateFileUploadDialog(file); - } else { - uploadFile(file); + if (file.exists()) { + ServerFilesFragment fragment = (ServerFilesFragment) + getSupportFragmentManager() + .findFragmentById(R.id.container_files); + if (fragment.checkForDuplicateFile(file.getName())) { + showDuplicateFileUploadDialog(file); + } else { + uploadFile(file); + } } } } @@ -461,7 +463,7 @@ public void onClick(DialogInterface dialog, int which) { } private void uploadFile(File uploadFile) { - serverClient.uploadFile(uploadFile, getShare(), file); + serverClient.uploadFile(0, uploadFile, getShare(), file); uploadProgressDialog.show(); } diff --git a/src/main/java/org/amahi/anywhere/activity/SettingsActivity.java b/src/main/java/org/amahi/anywhere/activity/SettingsActivity.java index 75980cffb..1139b1774 100644 --- a/src/main/java/org/amahi/anywhere/activity/SettingsActivity.java +++ b/src/main/java/org/amahi/anywhere/activity/SettingsActivity.java @@ -19,21 +19,25 @@ package org.amahi.anywhere.activity; -import android.os.Bundle; import android.app.FragmentManager; import android.app.FragmentTransaction; +import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.MenuItem; +import com.squareup.otto.Subscribe; + import org.amahi.anywhere.R; +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.UploadSettingsOpeningEvent; import org.amahi.anywhere.fragment.SettingsFragment; +import org.amahi.anywhere.fragment.UploadSettingsFragment; /** * Settings activity. Shows application's settings. * Settings itself are provided via {@link org.amahi.anywhere.fragment.SettingsFragment}. */ -public class SettingsActivity extends AppCompatActivity -{ +public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -53,18 +57,41 @@ private void setUpHomeNavigation() { private void setUpSettingsFragment() { FragmentManager fragmentManager = getFragmentManager(); fragmentManager.beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - .replace(R.id.settings_container,new SettingsFragment()).commit(); + .replace(R.id.settings_container, new SettingsFragment()).commit(); } @Override public boolean onOptionsItemSelected(MenuItem menuItem) { switch (menuItem.getItemId()) { case android.R.id.home: - finish(); + onBackPressed(); return true; default: return super.onOptionsItemSelected(menuItem); } } + + @Subscribe + public void onUploadSettingsOpenEvent(UploadSettingsOpeningEvent event) { + getFragmentManager().beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .replace(R.id.settings_container, new UploadSettingsFragment()) + .addToBackStack(null) + .commit(); + } + + @Override + protected void onResume() { + super.onResume(); + + BusProvider.getBus().register(this); + } + + @Override + protected void onPause() { + super.onPause(); + + BusProvider.getBus().unregister(this); + } } diff --git a/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java index 2e45052d3..23f934743 100644 --- a/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java +++ b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.java @@ -20,13 +20,19 @@ package org.amahi.anywhere.bus; public class ServerFileUploadCompleteEvent implements BusEvent { - private boolean wasUploadSuccessful; + private int id; + private boolean wasUploadSuccessful; - public ServerFileUploadCompleteEvent(boolean wasUploadSuccessful) { - this.wasUploadSuccessful = wasUploadSuccessful; - } + public ServerFileUploadCompleteEvent(int id, boolean wasUploadSuccessful) { + this.id = id; + this.wasUploadSuccessful = wasUploadSuccessful; + } - public boolean wasUploadSuccessful() { - return wasUploadSuccessful; - } + public int getId() { + return id; + } + + public boolean wasUploadSuccessful() { + return wasUploadSuccessful; + } } diff --git a/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java index c57391fe6..d183935c1 100644 --- a/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java +++ b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.java @@ -20,13 +20,19 @@ package org.amahi.anywhere.bus; public class ServerFileUploadProgressEvent implements BusEvent { - private int progress; + private int id; + private int progress; - public ServerFileUploadProgressEvent(int progress) { - this.progress = progress; - } + public ServerFileUploadProgressEvent(int id, int progress) { + this.id = id; + this.progress = progress; + } - public int getProgress() { - return this.progress; - } + public int getId() { + return this.id; + } + + public int getProgress() { + return this.progress; + } } diff --git a/src/main/java/org/amahi/anywhere/bus/UploadSettingsOpeningEvent.java b/src/main/java/org/amahi/anywhere/bus/UploadSettingsOpeningEvent.java new file mode 100644 index 000000000..43ebb29a9 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/bus/UploadSettingsOpeningEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.bus; + +public class UploadSettingsOpeningEvent implements BusEvent { +} diff --git a/src/main/java/org/amahi/anywhere/db/UploadQueueDb.java b/src/main/java/org/amahi/anywhere/db/UploadQueueDb.java new file mode 100644 index 000000000..db3246544 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/db/UploadQueueDb.java @@ -0,0 +1,44 @@ +package org.amahi.anywhere.db; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * SQLite db for maintaining image uploads in a persistent database. + * Query methods managed by {@link UploadQueueDbHelper UploadQueueDbHelper}. + */ + +class UploadQueueDb extends SQLiteOpenHelper { + + // Database version + private static final int DATABASE_VERSION = 1; + + // Database Name + private static final String DATABASE_NAME = "AMAHI_ANYWHERE_DATABASE"; + + // Table name + static final String TABLE_NAME = "UPLOAD_QUEUE_TABLE"; + + // column names + static final String KEY_ID = "id"; + static final String KEY_FILE_PATH = "file_path"; + + UploadQueueDb(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + KEY_FILE_PATH + " VARCHAR(200) NOT NULL)"; + + db.execSQL(CREATE_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + +} diff --git a/src/main/java/org/amahi/anywhere/db/UploadQueueDbHelper.java b/src/main/java/org/amahi/anywhere/db/UploadQueueDbHelper.java new file mode 100644 index 000000000..4fff60f8b --- /dev/null +++ b/src/main/java/org/amahi/anywhere/db/UploadQueueDbHelper.java @@ -0,0 +1,94 @@ +package org.amahi.anywhere.db; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.amahi.anywhere.model.UploadFile; + +import java.util.ArrayList; + +import static org.amahi.anywhere.db.UploadQueueDb.KEY_ID; +import static org.amahi.anywhere.db.UploadQueueDb.TABLE_NAME; + +/** + * Performs CRUD operation on SQLite db provided by {@link UploadQueueDb UploadQueueDb}. + */ + +public class UploadQueueDbHelper { + + private UploadQueueDb uploadQueueDb; + private SQLiteDatabase sqLiteDatabase; + private static UploadQueueDbHelper uploadQueueDbHelper; + + public static UploadQueueDbHelper init(Context context) { + if (uploadQueueDbHelper == null) uploadQueueDbHelper = new UploadQueueDbHelper(context); + return uploadQueueDbHelper; + } + + private UploadQueueDbHelper(Context context) { + uploadQueueDb = new UploadQueueDb(context); + sqLiteDatabase = uploadQueueDb.getWritableDatabase(); + } + + public UploadFile addNewImagePath(String imagePath) { + ContentValues values = new ContentValues(); + + values.put(UploadQueueDb.KEY_FILE_PATH, imagePath); + int id = (int) sqLiteDatabase.insert(TABLE_NAME, null, values); + if (id != -1) { + return new UploadFile(id, imagePath); + } else { + return null; + } + } + + public ArrayList getAllImagePaths() { + ArrayList imagePaths = new ArrayList<>(); + + Cursor cursor = sqLiteDatabase.query(TABLE_NAME, null, null, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + int id = cursor.getInt( + cursor.getColumnIndex(UploadQueueDb.KEY_ID)); + String imagePath = cursor.getString( + cursor.getColumnIndex(UploadQueueDb.KEY_FILE_PATH)); + + UploadFile uploadFile = new UploadFile(id, imagePath); + imagePaths.add(uploadFile); + cursor.moveToNext(); + } + } + if (cursor != null) { + cursor.close(); + } + + return imagePaths; + } + + public void removeFirstImagePath() { + Cursor cursor = sqLiteDatabase.query(TABLE_NAME, null, null, null, null, null, null); + if (cursor.moveToFirst()) { + String rowId = cursor.getString(cursor.getColumnIndex(KEY_ID)); + + sqLiteDatabase.delete(TABLE_NAME, KEY_ID + "=?", new String[]{rowId}); + } + cursor.close(); + } + + public void removeImagePath(int id) { + sqLiteDatabase.delete(TABLE_NAME, KEY_ID + "=?", new String[]{String.valueOf(id)}); + } + + public void clearDb() { + sqLiteDatabase.execSQL("DELETE FROM " + TABLE_NAME); + } + + public void closeDataBase() { + sqLiteDatabase.close(); + uploadQueueDb.close(); + uploadQueueDbHelper = null; + } + +} diff --git a/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java index 2b5e944d7..b68fdbb87 100644 --- a/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java +++ b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java @@ -705,14 +705,6 @@ public boolean checkForDuplicateFile(String fileName) { return false; } - public void refreshFileList() { - if (!isMetadataAvailable()) { - getFilesAdapter().notifyDataSetChanged(); - } else { - getFilesAdapter().notifyDataSetChanged(); - } - } - @Override public void onResume() { super.onResume(); diff --git a/src/main/java/org/amahi/anywhere/fragment/SettingsFragment.java b/src/main/java/org/amahi/anywhere/fragment/SettingsFragment.java index 236308d78..321a353a4 100644 --- a/src/main/java/org/amahi/anywhere/fragment/SettingsFragment.java +++ b/src/main/java/org/amahi/anywhere/fragment/SettingsFragment.java @@ -29,6 +29,7 @@ import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.widget.Toast; @@ -37,6 +38,8 @@ import org.amahi.anywhere.R; import org.amahi.anywhere.account.AmahiAccount; import org.amahi.anywhere.activity.NavigationActivity; +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.UploadSettingsOpeningEvent; import org.amahi.anywhere.server.ApiConnection; import org.amahi.anywhere.server.client.ServerClient; import org.amahi.anywhere.util.Android; @@ -51,9 +54,8 @@ * Settings fragment. Shows application's settings. */ public class SettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, - SharedPreferences.OnSharedPreferenceChangeListener, - AccountManagerCallback -{ + SharedPreferences.OnSharedPreferenceChangeListener, + AccountManagerCallback { @Inject ServerClient serverClient; @@ -68,6 +70,10 @@ private void setUpInjections() { AmahiApplication.from(getActivity()).inject(this); } + private void setUpTitle() { + getActivity().setTitle(R.string.title_settings); + } + private void setUpSettings() { setUpSettingsContent(); setUpSettingsSummary(); @@ -81,9 +87,11 @@ private void setUpSettingsContent() { private void setUpSettingsSummary() { ListPreference serverConnection = (ListPreference) getPreference(R.string.preference_key_server_connection); Preference applicationVersion = getPreference(R.string.preference_key_about_version); + Preference autoUpload = getPreference(R.string.preference_screen_key_upload); serverConnection.setSummary(getServerConnectionSummary()); applicationVersion.setSummary(getApplicationVersionSummary()); + autoUpload.setSummary(getAutoUploadSummary()); } private String getServerConnectionSummary() { @@ -92,14 +100,24 @@ private String getServerConnectionSummary() { return String.format("%s", serverConnection.getEntry()); } - private Preference getPreference(int id){ + private Preference getPreference(int id) { return findPreference(getString(id)); } private String getApplicationVersionSummary() { return String.format( - "Amahi for Android %s\nwww.amahi.org/android", - Android.getApplicationVersion()); + "Amahi for Android %s\nwww.amahi.org/android", + Android.getApplicationVersion()); + } + + private String getAutoUploadSummary() { + return isUploadEnabled() ? "Enabled" : "Disabled"; + } + + private boolean isUploadEnabled() { + PreferenceManager preferenceManager = getPreferenceManager(); + return preferenceManager.getSharedPreferences() + .getBoolean(getString(R.string.preference_key_upload_switch), false); } private void setUpSettingsListeners() { @@ -108,39 +126,38 @@ private void setUpSettingsListeners() { Preference applicationFeedback = getPreference(R.string.preference_key_about_feedback); Preference applicationRating = getPreference(R.string.preference_key_about_rating); Preference shareApp = getPreference(R.string.preference_key_tell_a_friend); + Preference autoUpload = getPreference(R.string.preference_screen_key_upload); accountSignOut.setOnPreferenceClickListener(this); applicationVersion.setOnPreferenceClickListener(this); applicationFeedback.setOnPreferenceClickListener(this); applicationRating.setOnPreferenceClickListener(this); shareApp.setOnPreferenceClickListener(this); + autoUpload.setOnPreferenceClickListener(this); } @Override public boolean onPreferenceClick(Preference preference) { if (preference.getKey().equals(getString(R.string.preference_key_account_sign_out))) { tearDownAccount(); - } - - if (preference.getKey().equals(getString(R.string.preference_key_about_version))) { + } else if (preference.getKey().equals(getString(R.string.preference_key_about_version))) { setUpApplicationVersion(); - } - - if (preference.getKey().equals(getString(R.string.preference_key_about_feedback))) { + } else if (preference.getKey().equals(getString(R.string.preference_key_about_feedback))) { setUpApplicationFeedback(); - } - - if (preference.getKey().equals(getString(R.string.preference_key_about_rating))) { + } else if (preference.getKey().equals(getString(R.string.preference_key_about_rating))) { setUpApplicationRating(); - } - - if (preference.getKey().equals(getString(R.string.preference_key_tell_a_friend))){ + } else if (preference.getKey().equals(getString(R.string.preference_key_tell_a_friend))) { sharedIntent(); + } else if (preference.getKey().equals(getString(R.string.preference_screen_key_upload))) { + openUploadSettingsFragment(); } - return true; } + private void openUploadSettingsFragment() { + BusProvider.getBus().post(new UploadSettingsOpeningEvent()); + } + private void tearDownAccount() { if (!getAccounts().isEmpty()) { Account account = getAccounts().get(0); @@ -172,13 +189,13 @@ private void tearDownActivity() { getActivity().finish(); } - private void sharedIntent(){ + private void sharedIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT,getString(R.string.share_subject)); - sendIntent.putExtra(Intent.EXTRA_TEXT,getString(R.string.share_message)); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_subject)); + sendIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_message)); sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent,getString(R.string.share_screen_title))); + startActivity(Intent.createChooser(sendIntent, getString(R.string.share_screen_title))); } private void setUpApplicationVersion() { @@ -190,9 +207,8 @@ private void setUpApplicationFeedback() { Intent intent = Intents.Builder.with(getActivity()).buildFeedbackIntent(); if (intent.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(intent); - } - else { - Snackbar.make(getView(),getString(R.string.application_not_found),Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(getView(), getString(R.string.application_not_found), Snackbar.LENGTH_SHORT).show(); } } @@ -200,9 +216,8 @@ private void setUpApplicationRating() { Intent intent = Intents.Builder.with(getActivity()).buildGooglePlayIntent(); if (intent.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(intent); - } - else { - Snackbar.make(getView(),getString(R.string.application_not_found),Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(getView(), getString(R.string.application_not_found), Snackbar.LENGTH_SHORT).show(); } } @@ -261,6 +276,7 @@ public void onResume() { super.onResume(); setUpSettingsPreferenceListener(); + setUpTitle(); } private void setUpSettingsPreferenceListener() { diff --git a/src/main/java/org/amahi/anywhere/fragment/UploadSettingsFragment.java b/src/main/java/org/amahi/anywhere/fragment/UploadSettingsFragment.java new file mode 100644 index 000000000..8c15236ce --- /dev/null +++ b/src/main/java/org/amahi/anywhere/fragment/UploadSettingsFragment.java @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2015 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.fragment; + +import android.Manifest; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.SwitchPreference; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.design.widget.Snackbar; +import android.view.View; + +import com.squareup.otto.Subscribe; + +import org.amahi.anywhere.AmahiApplication; +import org.amahi.anywhere.R; +import org.amahi.anywhere.account.AmahiAccount; +import org.amahi.anywhere.activity.ServerFilesActivity; +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.ServerConnectedEvent; +import org.amahi.anywhere.bus.ServerConnectionChangedEvent; +import org.amahi.anywhere.bus.ServerSharesLoadedEvent; +import org.amahi.anywhere.bus.ServersLoadedEvent; +import org.amahi.anywhere.server.client.AmahiClient; +import org.amahi.anywhere.server.client.ServerClient; +import org.amahi.anywhere.server.model.Server; +import org.amahi.anywhere.server.model.ServerShare; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.inject.Inject; + +import pub.devrel.easypermissions.AppSettingsDialog; +import pub.devrel.easypermissions.EasyPermissions; + +/** + * Upload Settings fragment. Shows upload settings. + */ +public class UploadSettingsFragment extends PreferenceFragment implements + Preference.OnPreferenceChangeListener, + AccountManagerCallback, + EasyPermissions.PermissionCallbacks { + + private static final int READ_PERMISSIONS = 110; + @Inject + AmahiClient amahiClient; + + @Inject + ServerClient serverClient; + + private String authenticationToken; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setUpInjections(); + + setUpTitle(); + + setUpSettings(); + } + + private void setUpInjections() { + AmahiApplication.from(getActivity()).inject(this); + } + + private void setUpTitle() { + getActivity().setTitle(R.string.preference_title_upload_settings); + } + + private void setUpSettings() { + setUpSettingsContent(); + setUpSettingsTitle(); + toggleUploadSettings(isUploadEnabled()); + setUpSettingsListeners(); + } + + private void setUpSettingsContent() { + addPreferencesFromResource(R.xml.upload_settings); + } + + private void setUpSettingsTitle() { + getAutoUploadSwitchPreference().setTitle(getAutoUploadTitle(isUploadEnabled())); + } + + private AccountManager getAccountManager() { + return AccountManager.get(getActivity()); + } + + private List getAccounts() { + return Arrays.asList(getAccountManager().getAccountsByType(AmahiAccount.TYPE)); + } + + private void setUpAuthenticationToken() { + if (authenticationToken != null) { + setUpServersContent(authenticationToken); + } else { + Account account = getAccounts().get(0); + getAccountManager().getAuthToken(account, AmahiAccount.TYPE, null, getActivity(), this, null); + } + + } + + @Override + public void run(AccountManagerFuture future) { + try { + Bundle accountManagerResult = future.getResult(); + + authenticationToken = accountManagerResult.getString(AccountManager.KEY_AUTHTOKEN); + + setUpAuthenticationToken(); + + } catch (OperationCanceledException e) { + tearDownActivity(); + } catch (IOException | AuthenticatorException e) { + throw new RuntimeException(e); + } + } + + private void setUpServersContent(String authenticationToken) { + amahiClient.getServers(getActivity(), authenticationToken); + } + + @Subscribe + public void onServersLoaded(ServersLoadedEvent event) { + setUpServersContent(event.getServers()); + } + + private void setUpServersContent(List servers) { + ArrayList activeServers = filterActiveServers(servers); + String[] serverNames = new String[activeServers.size()]; + String[] serverSessions = new String[activeServers.size()]; + + for (int i = 0; i < activeServers.size(); i++) { + Server activeServer = activeServers.get(i); + serverNames[i] = activeServer.getName(); + serverSessions[i] = activeServer.getSession(); + } + + getHdaPreference().setEntries(serverNames); + getHdaPreference().setEntryValues(serverSessions); + getHdaPreference().setEnabled(true); + + String session = getHdaPreference().getValue(); + if (session != null) { + setUpServer(session); + } + } + + private ArrayList filterActiveServers(List servers) { + ArrayList activeServers = new ArrayList<>(); + + for (Server server : servers) { + if (server.isActive()) { + activeServers.add(server); + } + } + + return activeServers; + } + + private boolean isUploadEnabled() { + PreferenceManager preferenceManager = getPreferenceManager(); + return preferenceManager.getSharedPreferences() + .getBoolean(getString(R.string.preference_key_upload_switch), false); + } + + private String getAutoUploadTitle(boolean isUploadEnabled) { + return isUploadEnabled ? "Disable" : "Enable"; + } + + private void setUpSettingsListeners() { + getAutoUploadSwitchPreference().setOnPreferenceChangeListener(this); + getHdaPreference().setOnPreferenceChangeListener(this); + getSharePreference().setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String key = preference.getKey(); + if (key.equals(getString(R.string.preference_key_upload_switch))) { + boolean isUploadEnabled = (boolean) newValue; + if (isUploadEnabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + checkReadPermissions(); + return false; + } + } + toggleUploadSettings(isUploadEnabled); + preference.setTitle(getAutoUploadTitle(isUploadEnabled)); + } else if (key.equals(getString(R.string.preference_key_upload_hda))) { + setUpServer(String.valueOf(newValue)); + } else if (key.equals(getString(R.string.preference_key_upload_share))) { + getPathPreference().setEnabled(true); + getAllowOnDataPreference().setEnabled(true); + } + return true; + } + + private void setUpServer(String session) { + getSharePreference().setEnabled(false); + getPathPreference().setEnabled(false); + getAllowOnDataPreference().setEnabled(false); + + Server server = new Server(session); + setUpServerConnection(server); + } + + @Subscribe + public void onServerConnected(ServerConnectedEvent event) { + setUpServerConnection(); + } + + private void setUpServerConnection(Server server) { + if (serverClient.isConnected(server)) { + setUpServerConnection(); + } else { + serverClient.connect(getActivity(), server); + } + } + + private void setUpServerConnection() { + if (!isConnectionAvailable() || isConnectionAuto()) { + serverClient.connectAuto(); + return; + } + + if (isConnectionLocal()) { + serverClient.connectLocal(); + } else { + serverClient.connectRemote(); + } + } + + + @Subscribe + public void onServerConnectionChanged(ServerConnectionChangedEvent event) { + setUpSharesContent(); + } + + private void setUpSharesContent() { + if (serverClient.isConnected()) { + serverClient.getShares(); + } + } + + @Subscribe + public void onSharesLoaded(ServerSharesLoadedEvent event) { + setUpSharesContent(event.getServerShares()); + } + + private void setUpSharesContent(List shares) { + String[] shareNames = new String[shares.size()]; + for (int i = 0; i < shares.size(); i++) { + shareNames[i] = shares.get(i).getName(); + } + getSharePreference().setEntries(shareNames); + getSharePreference().setEntryValues(shareNames); + getSharePreference().setEnabled(true); + + String selectedShare = getSharePreference().getValue(); + if (selectedShare != null) { + getPathPreference().setEnabled(true); + getAllowOnDataPreference().setEnabled(true); + } + } + + private boolean isConnectionAvailable() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + + return preferences.contains(getString(R.string.preference_key_server_connection)); + } + + private boolean isConnectionAuto() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + String preferenceConnection = preferences.getString(getString(R.string.preference_key_server_connection), null); + + return preferenceConnection.equals(getString(R.string.preference_key_server_connection_auto)); + } + + private boolean isConnectionLocal() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + String preferenceConnection = preferences.getString(getString(R.string.preference_key_server_connection), null); + + return preferenceConnection.equals(getString(R.string.preference_key_server_connection_local)); + } + + private void toggleUploadSettings(boolean isUploadEnabled) { + if (isUploadEnabled) { + setUpAuthenticationToken(); + } else { + getHdaPreference().setEnabled(false); + getSharePreference().setEnabled(false); + getPathPreference().setEnabled(false); + getAllowOnDataPreference().setEnabled(false); + } + } + + private Preference getPreference(int id) { + return findPreference(getString(id)); + } + + private SwitchPreference getAutoUploadSwitchPreference() { + return (SwitchPreference) getPreference(R.string.preference_key_upload_switch); + } + + private ListPreference getHdaPreference() { + return (ListPreference) getPreference(R.string.preference_key_upload_hda); + } + + private ListPreference getSharePreference() { + return (ListPreference) getPreference(R.string.preference_key_upload_share); + } + + private EditTextPreference getPathPreference() { + return (EditTextPreference) getPreference(R.string.preference_key_upload_path); + } + + private SwitchPreference getAllowOnDataPreference() { + return (SwitchPreference) getPreference(R.string.preference_key_upload_data); + } + + private void tearDownActivity() { + getActivity().finish(); + } + + + @Override + public void onResume() { + super.onResume(); + + BusProvider.getBus().register(this); + } + + @Override + public void onPause() { + super.onPause(); + + BusProvider.getBus().unregister(this); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + // Forward results to EasyPermissions + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); + } + + @Override + public void onPermissionsGranted(int requestCode, List perms) { + if (requestCode == READ_PERMISSIONS) { + toggleUploadSettings(true); + getAutoUploadSwitchPreference().setTitle(getAutoUploadTitle(true)); + } + } + + @Override + public void onPermissionsDenied(int requestCode, List perms) { + if (requestCode == READ_PERMISSIONS) { + showPermissionSnackBar(getString(R.string.file_upload_permission_denied)); + } + } + + private void showPermissionSnackBar(String message) { + Snackbar.make(getView(), message, Snackbar.LENGTH_LONG) + .setAction(R.string.menu_settings, new View.OnClickListener() { + @Override + public void onClick(View v) { + new AppSettingsDialog.Builder(UploadSettingsFragment.this).build().show(); + } + }) + .show(); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private void checkReadPermissions() { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + if (!EasyPermissions.hasPermissions(getContext(), perms)) { + EasyPermissions.requestPermissions(this, getString(R.string.file_upload_permission), + READ_PERMISSIONS, perms); + } + } +} diff --git a/src/main/java/org/amahi/anywhere/job/NetConnectivityJob.java b/src/main/java/org/amahi/anywhere/job/NetConnectivityJob.java new file mode 100644 index 000000000..2e5f08a08 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/job/NetConnectivityJob.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.job; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import android.support.annotation.RequiresApi; +import android.util.Log; + +import org.amahi.anywhere.AmahiApplication.JobIds; +import org.amahi.anywhere.service.UploadService; +import org.amahi.anywhere.util.Intents; + +import java.util.ArrayList; +import java.util.List; + +/** + * Job to monitor when there is a change to photos in the media provider. + */ +@RequiresApi(api = Build.VERSION_CODES.N) +public class NetConnectivityJob extends JobService { + private final String TAG = this.getClass().getName(); + + // A pre-built JobInfo we use for scheduling our job. + static final JobInfo JOB_INFO; + + static { + JobInfo.Builder builder = new JobInfo.Builder(JobIds.NET_CONNECTIVITY_JOB, + new ComponentName("org.amahi.anywhere", NetConnectivityJob.class.getName())); + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + JOB_INFO = builder.build(); + } + + // Schedule this job, replace any existing one. + public static void scheduleJob(Context context) { + JobScheduler js = context.getSystemService(JobScheduler.class); + js.schedule(JOB_INFO); + Log.i("NetworkConnectivityJob", "JOB SCHEDULED!"); + } + + // Check whether this job is currently scheduled. + public static boolean isScheduled(Context context) { + JobScheduler js = context.getSystemService(JobScheduler.class); + JobInfo job = js.getPendingJob(JobIds.NET_CONNECTIVITY_JOB); + return job != null; + } + + // Cancel this job, if currently scheduled. + public static void cancelJob(Context context) { + JobScheduler js = context.getSystemService(JobScheduler.class); + js.cancel(JobIds.NET_CONNECTIVITY_JOB); + } + + @Override + public boolean onStartJob(JobParameters params) { + Log.i(TAG, "JOB STARTED!"); + Intent intent = new Intent(this, UploadService.class); + startService(intent); + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } +} diff --git a/src/main/java/org/amahi/anywhere/job/PhotosContentJob.java b/src/main/java/org/amahi/anywhere/job/PhotosContentJob.java new file mode 100644 index 000000000..b734e717d --- /dev/null +++ b/src/main/java/org/amahi/anywhere/job/PhotosContentJob.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.job; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import android.support.annotation.RequiresApi; +import android.util.Log; + +import org.amahi.anywhere.AmahiApplication.JobIds; +import org.amahi.anywhere.util.Intents; + +import java.util.ArrayList; +import java.util.List; + +/** + * Job to monitor when there is a change to photos in the media provider. + */ +@RequiresApi(api = Build.VERSION_CODES.N) +public class PhotosContentJob extends JobService { + private final String TAG = this.getClass().getName(); + + // The root URI of the media provider, to monitor for generic changes to its content. + static final Uri MEDIA_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/"); + + // Path segments for image-specific URIs in the provider. + static final List EXTERNAL_PATH_SEGMENTS + = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.getPathSegments(); + + // A pre-built JobInfo we use for scheduling our job. + static final JobInfo JOB_INFO; + + static { + JobInfo.Builder builder = new JobInfo.Builder(JobIds.PHOTOS_CONTENT_JOB, + new ComponentName("org.amahi.anywhere", PhotosContentJob.class.getName())); + // Look for specific changes to images in the provider. + builder.addTriggerContentUri(new JobInfo.TriggerContentUri( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); + // Also look for general reports of changes in the overall provider. + builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MEDIA_URI, 0)); + JOB_INFO = builder.build(); + } + + JobParameters mRunningParams; + + // Schedule this job, replace any existing one. + public static void scheduleJob(Context context) { + JobScheduler js = context.getSystemService(JobScheduler.class); + js.schedule(JOB_INFO); + Log.i("PhotosContentJob", "JOB SCHEDULED!"); + } + + // Check whether this job is currently scheduled. + public static boolean isScheduled(Context context) { + JobScheduler js = context.getSystemService(JobScheduler.class); + JobInfo job = js.getPendingJob(JobIds.PHOTOS_CONTENT_JOB); + return job != null; + } + + // Cancel this job, if currently scheduled. + public static void cancelJob(Context context) { + JobScheduler js = context.getSystemService(JobScheduler.class); + js.cancel(JobIds.PHOTOS_CONTENT_JOB); + } + + @Override + public boolean onStartJob(JobParameters params) { + Log.i("PhotosContentJob", "JOB STARTED!"); + mRunningParams = params; + + // Did we trigger due to a content change? + if (params.getTriggeredContentAuthorities() != null) { + if (params.getTriggeredContentUris() != null) { + // If we have details about which URIs changed, then iterate through them + // and collect valid uris and send them to UploadService + ArrayList uris = new ArrayList<>(); + for (Uri uri : params.getTriggeredContentUris()) { + List path = uri.getPathSegments(); + if (path != null && path.size() == EXTERNAL_PATH_SEGMENTS.size() + 1) { + // This is a specific file. + uris.add(uri); + } + } + Intent intent = Intents.Builder.with(this).buildUploadServiceIntent(uris); + startService(intent); + } else { + // We don't have any details about URIs (because too many changed at once) + Log.i(TAG, "Photos rescan needed!"); + } + } else { + Log.i(TAG, "(No photos content)"); + } + + scheduleJob(PhotosContentJob.this); + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } +} diff --git a/src/main/java/org/amahi/anywhere/model/UploadFile.java b/src/main/java/org/amahi/anywhere/model/UploadFile.java new file mode 100644 index 000000000..6b24344f3 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/model/UploadFile.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.model; + +public class UploadFile { + private int id; + private String path; + + public UploadFile(int id, String path) { + this.id = id; + this.path = path; + } + + public int getId() { + return id; + } + + public String getPath() { + return path; + } +} diff --git a/src/main/java/org/amahi/anywhere/receiver/CameraReceiver.java b/src/main/java/org/amahi/anywhere/receiver/CameraReceiver.java new file mode 100644 index 000000000..f0a8a7e23 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/receiver/CameraReceiver.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import org.amahi.anywhere.service.UploadService; +import org.amahi.anywhere.util.Intents; + +import java.util.ArrayList; + +/** + * Camera new picture event receiver. + */ +public class CameraReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Log.e("NEW_IMAGE", "onReceive"); + Intent uploadService = Intents.Builder.with(context).buildUploadServiceIntent(intent.getData()); + context.startService(uploadService); + } +} diff --git a/src/main/java/org/amahi/anywhere/receiver/NetworkReceiver.java b/src/main/java/org/amahi/anywhere/receiver/NetworkReceiver.java index 5c4d7489b..a1054082d 100644 --- a/src/main/java/org/amahi/anywhere/receiver/NetworkReceiver.java +++ b/src/main/java/org/amahi/anywhere/receiver/NetworkReceiver.java @@ -27,13 +27,14 @@ import org.amahi.anywhere.bus.BusProvider; import org.amahi.anywhere.bus.NetworkChangedEvent; +import org.amahi.anywhere.service.UploadService; +import org.amahi.anywhere.util.NetworkUtils; /** * Network system events receiver. Proxies system network events such as changing network connection * to the local {@link com.squareup.otto.Bus} as {@link org.amahi.anywhere.bus.BusEvent}. */ -public class NetworkReceiver extends BroadcastReceiver -{ +public class NetworkReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { @@ -42,22 +43,28 @@ public void onReceive(Context context, Intent intent) { } private void handleNetworkChangeEvent(Context context) { - NetworkInfo network = getNetwork(context); - - if (isNetworkConnected(network)) { + NetworkUtils networkUtils = new NetworkUtils(context); + NetworkInfo network = networkUtils.getNetwork(); + if (networkUtils.isNetworkConnected(network)) { BusProvider.getBus().post(new NetworkChangedEvent(network.getType())); } - } - private NetworkInfo getNetwork(Context context) { - return getNetworkManager(context).getActiveNetworkInfo(); + if (networkUtils.isUploadAllowed()) { + startUploadService(context); + } else { + stopUploadService(context); + } } - private ConnectivityManager getNetworkManager(Context context) { - return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + private void startUploadService(Context context) { + Intent uploadService = new Intent(context, UploadService.class); + context.startService(uploadService); } - private boolean isNetworkConnected(NetworkInfo network) { - return (network != null) && network.isConnected(); + private void stopUploadService(Context context) { + Intent uploadService = new Intent(context, UploadService.class); + context.stopService(uploadService); } + + } diff --git a/src/main/java/org/amahi/anywhere/server/client/ServerClient.java b/src/main/java/org/amahi/anywhere/server/client/ServerClient.java index b6c3351d0..afe1c6853 100644 --- a/src/main/java/org/amahi/anywhere/server/client/ServerClient.java +++ b/src/main/java/org/amahi/anywhere/server/client/ServerClient.java @@ -29,6 +29,7 @@ import org.amahi.anywhere.bus.ServerConnectedEvent; import org.amahi.anywhere.bus.ServerConnectionChangedEvent; import org.amahi.anywhere.bus.ServerConnectionDetectedEvent; +import org.amahi.anywhere.bus.ServerFileUploadCompleteEvent; import org.amahi.anywhere.bus.ServerRouteLoadedEvent; import org.amahi.anywhere.server.Api; import org.amahi.anywhere.server.ApiAdapter; @@ -54,12 +55,15 @@ import org.json.JSONObject; import java.io.File; +import java.io.IOException; import javax.inject.Inject; import javax.inject.Singleton; import okhttp3.MultipartBody; +import okhttp3.ResponseBody; import retrofit2.Callback; +import retrofit2.Response; import static org.amahi.anywhere.util.Android.loadServersFromAsset; @@ -242,20 +246,45 @@ public void deleteFile(ServerShare share, ServerFile serverFile) { .enqueue(new ServerFileDeleteResponse()); } - public void uploadFile(File file, ServerShare share) { - this.uploadFile(file, share, null); + private MultipartBody.Part createFilePart(int id, File file) { + return MultipartBody.Part.createFormData("file", + file.getName(), + new ProgressRequestBody(id, file)); } - public void uploadFile(File file, ServerShare share, ServerFile directory) { - MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", - file.getName(), - new ProgressRequestBody(file)); + public void uploadFile(int id, File file, String shareName, String path) { + MultipartBody.Part filePart = createFilePart(id, file); + uploadFileAsync(id, filePart, shareName, path); + } + + public void uploadFile(int id, File file, ServerShare share, ServerFile directory) { + MultipartBody.Part filePart = createFilePart(id, file); String path = "/"; if (directory != null) path = directory.getPath(); + uploadFileAsync(id, filePart, share.getName(), path); + } - serverApi.uploadFile(server.getSession(), share.getName(), path, filePart) - .enqueue(new ServerFileUploadResponse()); + private void uploadFileAsync(int id, MultipartBody.Part filePart, String shareName, String path) { + serverApi.uploadFile(server.getSession(), shareName, path, filePart) + .enqueue(new ServerFileUploadResponse(id)); + } + + private void uploadFileSync(int id, MultipartBody.Part filePart, String shareName, String path) { + try { + Response response = serverApi + .uploadFile(server.getSession(), shareName, path, filePart) + .execute(); + if (response.isSuccessful()) { + BusProvider.getBus().post( + new ServerFileUploadCompleteEvent(id, true)); + } else { + BusProvider.getBus().post( + new ServerFileUploadCompleteEvent(id, false)); + } + } catch (IOException e) { + e.printStackTrace(); + } } public Uri getFileUri(ServerShare share, ServerFile file) { diff --git a/src/main/java/org/amahi/anywhere/server/model/Server.java b/src/main/java/org/amahi/anywhere/server/model/Server.java index 42df9fe0c..5cb6d81ea 100644 --- a/src/main/java/org/amahi/anywhere/server/model/Server.java +++ b/src/main/java/org/amahi/anywhere/server/model/Server.java @@ -82,6 +82,10 @@ public Server(int index, String name, String session) { this.debug = true; } + public Server(String session) { + this.session = session; + } + public Server(Parcel parcel) { this.name = parcel.readString(); this.session = parcel.readString(); diff --git a/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java b/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java index e58e5ad7e..25fc0354d 100644 --- a/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java +++ b/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java @@ -34,16 +34,22 @@ * as {@link org.amahi.anywhere.bus.BusEvent}. */ public class ServerFileUploadResponse implements Callback { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - BusProvider.getBus().post(new ServerFileUploadCompleteEvent(true)); - } else - this.onFailure(call, new HttpException(response)); - } - - @Override - public void onFailure(Call call, Throwable t) { - BusProvider.getBus().post(new ServerFileUploadCompleteEvent(false)); - } + private int id; + + public ServerFileUploadResponse(int id) { + this.id = id; + } + + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + BusProvider.getBus().post(new ServerFileUploadCompleteEvent(id, true)); + } else + this.onFailure(call, new HttpException(response)); + } + + @Override + public void onFailure(Call call, Throwable t) { + BusProvider.getBus().post(new ServerFileUploadCompleteEvent(id, false)); + } } diff --git a/src/main/java/org/amahi/anywhere/service/UploadService.java b/src/main/java/org/amahi/anywhere/service/UploadService.java new file mode 100644 index 000000000..ec6ba5e5a --- /dev/null +++ b/src/main/java/org/amahi/anywhere/service/UploadService.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.service; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; + +import com.squareup.otto.Subscribe; + +import org.amahi.anywhere.AmahiApplication; +import org.amahi.anywhere.R; +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.ServerConnectedEvent; +import org.amahi.anywhere.bus.ServerConnectionChangedEvent; +import org.amahi.anywhere.db.UploadQueueDbHelper; +import org.amahi.anywhere.job.NetConnectivityJob; +import org.amahi.anywhere.model.UploadFile; +import org.amahi.anywhere.server.client.ServerClient; +import org.amahi.anywhere.server.model.Server; +import org.amahi.anywhere.util.Intents; +import org.amahi.anywhere.util.NetworkUtils; +import org.amahi.anywhere.util.UploadManager; + +import java.util.ArrayList; + +import javax.inject.Inject; + +/** + * File upload service + */ +public class UploadService extends Service implements UploadManager.UploadCallbacks { + + @Inject + ServerClient serverClient; + + private UploadManager uploadManager; + private UploadQueueDbHelper uploadQueueDbHelper; + private NotificationCompat.Builder notificationBuilder; + private NetworkUtils networkUtils; + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + setUpInjections(); + setUpBus(); + setUpDbHelper(); + setUpNetworkUtils(); + } + + private void setUpInjections() { + AmahiApplication.from(this).inject(this); + } + + private void setUpBus() { + BusProvider.getBus().register(this); + } + + private void setUpDbHelper() { + uploadQueueDbHelper = UploadQueueDbHelper.init(this); + } + + private void setUpNetworkUtils() { + networkUtils = new NetworkUtils(this); + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + + if (intent != null && intent.hasExtra(Intents.Extras.IMAGE_URIS)) { + if (isAutoUploadEnabled()) { + ArrayList uris = intent.getParcelableArrayListExtra(Intents.Extras.IMAGE_URIS); + for (Uri uri : uris) { + String imagePath = queryImagePath(uri); + if (imagePath != null) { + UploadFile uploadFile = uploadQueueDbHelper.addNewImagePath(imagePath); + if (uploadFile != null && uploadManager != null) + uploadManager.add(uploadFile); + } + } + } + } + + if (isAutoUploadEnabled()) { + if (isUploadAllowed()) { + connectToServer(); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + NetConnectivityJob.scheduleJob(this); + } + } + } + + return super.onStartCommand(intent, flags, startId); + } + + private boolean isAutoUploadEnabled() { + return PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.preference_key_upload_switch), false); + } + + private boolean isUploadAllowed() { + return networkUtils.isUploadAllowed(); + } + + private void connectToServer() { + Server server = getUploadServer(); + if (server != null) { + setUpServerConnection(server); + } + } + + private void setUpServerConnection(@NonNull Server server) { + if (serverClient.isConnected(server)) { + setUpServerConnection(); + } else { + serverClient.connect(this, server); + } + } + + @Subscribe + public void onServerConnected(ServerConnectedEvent event) { + Server uploadServer = getUploadServer(); + if (uploadServer != null && uploadServer == event.getServer()) { + setUpServerConnection(); + } + } + + private void setUpServerConnection() { + if (!isConnectionAvailable() || isConnectionAuto()) { + serverClient.connectAuto(); + return; + } + + if (isConnectionLocal()) { + serverClient.connectLocal(); + } else { + serverClient.connectRemote(); + } + } + + private boolean isConnectionAvailable() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + + return preferences.contains(getString(R.string.preference_key_server_connection)); + } + + private boolean isConnectionAuto() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + String preferenceConnection = preferences.getString(getString(R.string.preference_key_server_connection), null); + + return preferenceConnection.equals(getString(R.string.preference_key_server_connection_auto)); + } + + private boolean isConnectionLocal() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + String preferenceConnection = preferences.getString(getString(R.string.preference_key_server_connection), null); + + return preferenceConnection.equals(getString(R.string.preference_key_server_connection_local)); + } + + @Subscribe + public void onServerConnectionChanged(ServerConnectionChangedEvent event) { + if (uploadManager == null) { + setUpUploadManager(); + } + uploadManager.startUploading(); + } + + private Server getUploadServer() { + String session = PreferenceManager.getDefaultSharedPreferences(this) + .getString(getString(R.string.preference_key_upload_hda), null); + if (session != null) { + return new Server(session); + } else { + return null; + } + } + + private void setUpUploadManager() { + ArrayList uploadFiles = uploadQueueDbHelper.getAllImagePaths(); + uploadManager = new UploadManager(this, uploadFiles); + } + + private String queryImagePath(Uri imageUri) { + String filePath = null; + if ("content".equals(imageUri.getScheme())) { + Cursor cursor = this.getContentResolver() + .query(imageUri, null, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + int columnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + filePath = cursor.getString(columnIndex); + cursor.close(); + } + } else { + filePath = imageUri.toString(); + } + return filePath; + } + + @Override + public void uploadStarted(int id, String fileName) { + notificationBuilder = new NotificationCompat.Builder(getApplicationContext()); + notificationBuilder + .setOngoing(true) + .setSmallIcon(R.drawable.ic_app_logo) + .setContentTitle(getString(R.string.notification_upload_title)) + .setContentText(getString(R.string.notification_upload_message, fileName)) + .setProgress(100, 0, false) + .build(); + Notification notification = notificationBuilder.build(); + startForeground(id, notification); + } + + @Override + public void uploadProgress(int id, int progress) { + NotificationManager notificationManager = (NotificationManager) getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder + .setProgress(100, progress, false); + Notification notification = notificationBuilder.build(); + notificationManager.notify(id, notification); + } + + @Override + public void uploadSuccess(int id) { + uploadComplete(id, getString(R.string.message_upload_success)); + } + + @Override + public void uploadError(int id) { + uploadComplete(id, getString(R.string.message_upload_error)); + } + + private void uploadComplete(int id, String title) { + stopForeground(false); + NotificationManager notificationManager = (NotificationManager) getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + + notificationBuilder + .setContentTitle(title) + .setOngoing(false) + .setProgress(0, 0, false); + + Notification notification = notificationBuilder.build(); + notificationManager.notify(id, notification); + } + + @Override + public void removeFileFromDb(int id) { + uploadQueueDbHelper.removeImagePath(id); + } + + @Override + public void uploadQueueFinished() { + tearDownUploadManager(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + uploadQueueDbHelper.closeDataBase(); + tearDownUploadManager(); + tearDownBus(); + } + + private void tearDownUploadManager() { + if (uploadManager != null) { + uploadManager.tearDownBus(); + uploadManager = null; + } + } + + public void tearDownBus() { + BusProvider.getBus().unregister(this); + } +} diff --git a/src/main/java/org/amahi/anywhere/util/Intents.java b/src/main/java/org/amahi/anywhere/util/Intents.java index a0be37013..e0cc8ca6a 100644 --- a/src/main/java/org/amahi/anywhere/util/Intents.java +++ b/src/main/java/org/amahi/anywhere/util/Intents.java @@ -42,6 +42,7 @@ import org.amahi.anywhere.server.model.ServerApp; import org.amahi.anywhere.server.model.ServerFile; import org.amahi.anywhere.server.model.ServerShare; +import org.amahi.anywhere.service.UploadService; import org.amahi.anywhere.tv.activity.ServerFileTvActivity; import org.amahi.anywhere.tv.activity.TVWebViewActivity; import org.amahi.anywhere.tv.activity.TvPlaybackOverlayActivity; @@ -61,6 +62,7 @@ public static final class Extras { public static final String SERVER_FILE = "server_file"; public static final String SERVER_FILES = "server_files"; public static final String SERVER_SHARE = "server_share"; + public static final String IMAGE_URIS = "image_uris"; private Extras() { } } @@ -228,5 +230,17 @@ public Intent buildMediaPickerIntent () { public Intent buildCameraIntent() { return new Intent(MediaStore.ACTION_IMAGE_CAPTURE); } + + public Intent buildUploadServiceIntent(Uri uri) { + ArrayList uris = new ArrayList<>(); + uris.add(uri); + return buildUploadServiceIntent(uris); + } + + public Intent buildUploadServiceIntent(ArrayList uris) { + Intent uploadService = new Intent(context, UploadService.class); + uploadService.putParcelableArrayListExtra(Extras.IMAGE_URIS, uris); + return uploadService; + } } } diff --git a/src/main/java/org/amahi/anywhere/util/NetworkUtils.java b/src/main/java/org/amahi/anywhere/util/NetworkUtils.java new file mode 100644 index 000000000..d430fc88c --- /dev/null +++ b/src/main/java/org/amahi/anywhere/util/NetworkUtils.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.util; + + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.preference.PreferenceManager; + +import org.amahi.anywhere.R; + +/** + * Network utility methods to check the current connected network + */ +public class NetworkUtils { + + private Context context; + + public NetworkUtils(Context context) { + this.context = context; + } + + public NetworkInfo getNetwork() { + return getNetworkManager().getActiveNetworkInfo(); + } + + private ConnectivityManager getNetworkManager() { + return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + public boolean isNetworkConnected(NetworkInfo network) { + return (network != null) && network.isConnected(); + } + + public boolean isUploadAllowed() { + NetworkInfo network = getNetwork(); + return isNetworkConnected(network) && + (network.getType() != ConnectivityManager.TYPE_MOBILE || + isUploadAllowedOnMobileData()); + } + + private boolean isUploadAllowedOnMobileData() { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.preference_key_upload_data), false); + } +} diff --git a/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java b/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java index 24eb0f385..b75052ddf 100644 --- a/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java +++ b/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java @@ -38,61 +38,68 @@ * for file upload. */ public class ProgressRequestBody extends RequestBody { - private File mFile; - - private static final int DEFAULT_BUFFER_SIZE = 2048; - - public ProgressRequestBody(final File file) { - mFile = file; - } - - @Override - public MediaType contentType() { - // Only for uploading images - return MediaType.parse("image/*"); - } - - @Override - public long contentLength() throws IOException { - return mFile.length(); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - long fileLength = mFile.length(); - byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; - FileInputStream in = new FileInputStream(mFile); - long uploaded = 0; - - //noinspection TryFinallyCanBeTryWithResources - try { - int read; - Handler handler = new Handler(Looper.getMainLooper()); - while ((read = in.read(buffer)) != -1) { - - uploaded += read; - sink.write(buffer, 0, read); - - // update progress on UI thread - handler.post(new ProgressUpdater(uploaded, fileLength)); - } - } finally { - in.close(); - } - } - - private class ProgressUpdater implements Runnable { - private long mUploaded; - private long mTotal; - - ProgressUpdater(long uploaded, long total) { - mUploaded = uploaded; - mTotal = total; - } - - @Override - public void run() { - BusProvider.getBus().post(new ServerFileUploadProgressEvent((int) (100 * mUploaded / mTotal))); - } - } + private int mId; + private File mFile; + private int lastProgress = 0; + + private static final int DEFAULT_BUFFER_SIZE = 2048; + + public ProgressRequestBody(int id, File file) { + mId = id; + mFile = new File(file.getPath()); + } + + @Override + public MediaType contentType() { + // Only for uploading images + return MediaType.parse("image/*"); + } + + @Override + public long contentLength() throws IOException { + return mFile.length(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + long fileLength = mFile.length(); + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + FileInputStream in = new FileInputStream(mFile); + long uploaded = 0; + + //noinspection TryFinallyCanBeTryWithResources + try { + int read; + Handler handler = new Handler(Looper.getMainLooper()); + while ((read = in.read(buffer)) != -1) { + + uploaded += read; + sink.write(buffer, 0, read); + + // update progress on UI thread + handler.post(new ProgressUpdater(uploaded, fileLength)); + } + } finally { + in.close(); + } + } + + private class ProgressUpdater implements Runnable { + private long mUploaded; + private long mTotal; + + ProgressUpdater(long uploaded, long total) { + mUploaded = uploaded; + mTotal = total; + } + + @Override + public void run() { + int progress = (int) (100 * mUploaded / mTotal); + if (lastProgress != progress) { + lastProgress = progress; + BusProvider.getBus().post(new ServerFileUploadProgressEvent(mId, progress)); + } + } + } } diff --git a/src/main/java/org/amahi/anywhere/util/UploadManager.java b/src/main/java/org/amahi/anywhere/util/UploadManager.java new file mode 100644 index 000000000..647a83e58 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/util/UploadManager.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.util; + +import android.content.Context; +import android.os.Handler; +import android.preference.PreferenceManager; + +import com.squareup.otto.Subscribe; + +import org.amahi.anywhere.AmahiApplication; +import org.amahi.anywhere.R; +import org.amahi.anywhere.bus.BusProvider; +import org.amahi.anywhere.bus.ServerFileUploadCompleteEvent; +import org.amahi.anywhere.bus.ServerFileUploadProgressEvent; +import org.amahi.anywhere.model.UploadFile; +import org.amahi.anywhere.server.client.ServerClient; + +import java.io.File; +import java.util.ArrayList; + +import javax.inject.Inject; + +/** + * An Upload Manager that manages all the uploads one by one present in the queue. + */ +public class UploadManager { + private boolean isRunning = false; + + @Inject + public ServerClient serverClient; + + private Context context; + private ArrayList uploadFiles; + private UploadCallbacks uploadCallbacks; + + public + UploadManager(T context, ArrayList uploadFiles) { + + this.context = context; + this.uploadCallbacks = context; + this.uploadFiles = uploadFiles; + + setUpInjections(); + setUpBus(); + } + + private void setUpInjections() { + AmahiApplication.from(context).inject(this); + } + + private void setUpBus() { + BusProvider.getBus().register(this); + } + + public void tearDownBus() { + BusProvider.getBus().unregister(this); + } + + public void startUploading() { + if (!isRunning) { + isRunning = true; + processNextFile(); + } + } + + private void processNextFile() { + if (uploadFiles.isEmpty()) { + isRunning = false; + uploadCallbacks.uploadQueueFinished(); + } else { + UploadFile currentFile = uploadFiles.remove(0); + upload(currentFile); + } + } + + private void upload(UploadFile uploadFile) { + File image = new File(uploadFile.getPath()); + if (image.exists()) { + uploadCallbacks.uploadStarted(uploadFile.getId(), image.getName()); + serverClient.uploadFile(uploadFile.getId(), image, getUploadShareName(), + getUploadPath()); + } else { + uploadCallbacks.removeFileFromDb(uploadFile.getId()); + processNextFile(); + } + + } + + private String getUploadShareName() { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.preference_key_upload_share), null); + } + + private String getUploadPath() { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.preference_key_upload_path), null); + } + + + public void add(UploadFile uploadFile) { + uploadFiles.add(uploadFile); + } + + @Subscribe + public void onFileUploadProgressEvent(ServerFileUploadProgressEvent event) { + uploadCallbacks.uploadProgress(event.getId(), event.getProgress()); + } + + @Subscribe + public void onFileUploadCompleteEvent(ServerFileUploadCompleteEvent event) { + if (event.wasUploadSuccessful()) { + uploadCallbacks.removeFileFromDb(event.getId()); + uploadCallbacks.uploadSuccess(event.getId()); + } else { + uploadCallbacks.uploadError(event.getId()); + } + + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + processNextFile(); + } + }, 500); + } + + public interface UploadCallbacks { + void uploadStarted(int id, String fileName); + + void uploadProgress(int id, int progress); + + void uploadSuccess(int id); + + void uploadError(int id); + + void removeFileFromDb(int id); + + void uploadQueueFinished(); + } +} diff --git a/src/main/res/values/preferences.xml b/src/main/res/values/preferences.xml index 3ad31b746..0792bc9a6 100644 --- a/src/main/res/values/preferences.xml +++ b/src/main/res/values/preferences.xml @@ -22,6 +22,7 @@ About Account Settings + Auto Upload Feedback Rate @@ -29,6 +30,13 @@ Sign out Connection Tell a friend + Auto Upload + + HDA Server + Share + Path + Allow on Cellular Data + Autodetect Remote @@ -43,6 +51,13 @@ remote local tell_a_friend + auto_upload_screen + + auto_upload_switch + auto_upload_hda + auto_upload_share + auto_upload_path + auto_upload_data @string/preference_entry_server_connection_auto diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 536f6810c..d52f0e37d 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -77,6 +77,10 @@ There was some error in uploading your file Overwrite existing file? The file %1$s already exists.\nAre you sure you want to replace the existing file? + Uploading new image + File %1$s + Upload Complete + Upload failed Check out my Amahi home server! I use the Amahi Home Server for storing, backing up and streaming all my files.\n\nCheck it out!\n\nhttps://www.amahi.org/ diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml index e7197a550..23ae988ee 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -23,7 +23,7 @@ + android:title="@string/preference_title_account_sign_out"/> @@ -34,7 +34,11 @@ android:entries="@array/preference_entries_server_connection" android:entryValues="@array/preference_entries_keys_server_connection" android:key="@string/preference_key_server_connection" - android:title="@string/preference_title_server_connection" /> + android:title="@string/preference_title_server_connection"/> + + @@ -42,19 +46,19 @@ + android:title="@string/preference_title_about_version"/> + android:title="@string/preference_title_about_feedback"/> + android:title="@string/preference_title_about_rating"/> + android:title="@string/preference_title_tell_a_friend"/> diff --git a/src/main/res/xml/upload_settings.xml b/src/main/res/xml/upload_settings.xml new file mode 100644 index 000000000..052e6b7a7 --- /dev/null +++ b/src/main/res/xml/upload_settings.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + From bd31a3b67593f5b53e9cc9bef4afd8c00cb8d199 Mon Sep 17 00:00:00 2001 From: Chirag Maheshwari Date: Sun, 20 Aug 2017 00:44:03 +0530 Subject: [PATCH 8/8] resolve TransactionTooLargeException on android 7 --- .../activity/ServerFileAudioActivity.java | 81 ++++--------------- 1 file changed, 15 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/amahi/anywhere/activity/ServerFileAudioActivity.java b/src/main/java/org/amahi/anywhere/activity/ServerFileAudioActivity.java index b0cb0d94d..0ad7c3393 100644 --- a/src/main/java/org/amahi/anywhere/activity/ServerFileAudioActivity.java +++ b/src/main/java/org/amahi/anywhere/activity/ServerFileAudioActivity.java @@ -84,16 +84,6 @@ public static boolean supports(String mime_type) { return SUPPORTED_FORMATS.contains(mime_type); } - private static final class State - { - private State() { - } - - public static final String AUDIO_TITLE = "audio_title"; - public static final String AUDIO_SUBTITLE = "audio_subtitle"; - public static final String AUDIO_ALBUM_ART = "audio_album_art"; - } - @Inject ServerClient serverClient; @@ -111,7 +101,7 @@ protected void onCreate(Bundle savedInstanceState) { setUpHomeNavigation(); - setUpAudio(savedInstanceState); + setUpAudio(); } private void setUpInjections() { @@ -123,10 +113,9 @@ private void setUpHomeNavigation() { getSupportActionBar().setIcon(R.drawable.ic_launcher); } - private void setUpAudio(Bundle state) { + private void setUpAudio() { setUpAudioFile(); setUpAudioTitle(); - setUpAudioMetadata(state); } private void setUpAudioFile() { @@ -141,24 +130,6 @@ private void setUpAudioTitle() { getSupportActionBar().setTitle(audioFile.getName()); } - private void setUpAudioMetadata(Bundle state) { - if (isAudioMetadataStateValid(state)) { - setUpAudioMetadataState(state); - - showAudioMetadata(); - } - } - - private boolean isAudioMetadataStateValid(Bundle state) { - return (state != null) && state.containsKey(State.AUDIO_TITLE); - } - - private void setUpAudioMetadataState(Bundle state) { - getAudioTitleView().setText(state.getString(State.AUDIO_TITLE)); - getAudioSubtitleView().setText(state.getString(State.AUDIO_SUBTITLE)); - getAudioAlbumArtView().setImageBitmap((Bitmap) state.getParcelable(State.AUDIO_ALBUM_ART)); - } - private TextView getAudioTitleView() { return (TextView) findViewById(R.id.text_title); } @@ -251,6 +222,7 @@ private boolean areAudioControlsAvailable() { private void setUpAudioPlayback() { if (audioService.isAudioStarted()) { showAudio(); + setUpAudioMetadata(); } else { audioService.startAudio(getShare(), getAudioFiles(), getFile()); } @@ -418,7 +390,9 @@ protected void onResume() { BusProvider.getBus().register(this); - setUpAudioMetadata(); + if (hasAudioFileChanged()) { + setUpAudioMetadata(); + } } private void showAudioControlsForced() { @@ -427,20 +401,22 @@ private void showAudioControlsForced() { } } + private boolean hasAudioFileChanged() { + return isAudioServiceAvailable() && !this.audioFile.equals(audioService.getAudioFile()); + } + private void setUpAudioMetadata() { if (!isAudioServiceAvailable()) { return; } - if (!this.audioFile.equals(audioService.getAudioFile())) { - this.audioFile = audioService.getAudioFile(); + this.audioFile = audioService.getAudioFile(); - tearDownAudioTitle(); - tearDownAudioMetadata(); + tearDownAudioTitle(); + tearDownAudioMetadata(); - setUpAudioTitle(); - setUpAudioMetadata(audioService.getAudioMetadataFormatter(), audioService.getAudioAlbumArt()); - } + setUpAudioTitle(); + setUpAudioMetadata(audioService.getAudioMetadataFormatter(), audioService.getAudioAlbumArt()); } private boolean isAudioServiceAvailable() { @@ -481,33 +457,6 @@ private void tearDownAudioServiceBind() { unbindService(this); } - @Override - protected void onSaveInstanceState(Bundle state) { - super.onSaveInstanceState(state); - - if (isAudioMetadataLoaded()) { - tearDownAudioMetadataState(state); - } - } - - private boolean isAudioMetadataLoaded() { - String audioTitle = getAudioTitleView().getText().toString(); - String audioSubtitle = getAudioSubtitleView().getText().toString(); - BitmapDrawable audioAlbumArt = (BitmapDrawable) getAudioAlbumArtView().getDrawable(); - - return !audioTitle.isEmpty() && !audioSubtitle.isEmpty() && (audioAlbumArt != null); - } - - private void tearDownAudioMetadataState(Bundle state) { - String audioTitle = getAudioTitleView().getText().toString(); - String audioSubtitle = getAudioSubtitleView().getText().toString(); - BitmapDrawable audioAlbumArt = (BitmapDrawable) getAudioAlbumArtView().getDrawable(); - - state.putString(State.AUDIO_TITLE, audioTitle); - state.putString(State.AUDIO_SUBTITLE, audioSubtitle); - state.putParcelable(State.AUDIO_ALBUM_ART, audioAlbumArt.getBitmap()); - } - @Override protected void onDestroy() { super.onDestroy();