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"
+ }
+ ]
+ ```
diff --git a/build.gradle b/build.gradle
index 8324ba52e..b6a3bfaa6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -100,6 +100,7 @@ def formatStringField(field) {
dependencies {
repositories {
+ jcenter()
mavenCentral()
mavenLocal()
maven { url 'https://maven.fabric.io/public' }
@@ -133,6 +134,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 c89ead2e5..7662b0897 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -46,6 +46,10 @@
+
+
@@ -62,6 +66,16 @@
android:theme="@style/Theme.Amahi"
android:banner="@drawable/tv_banner">
+
+
+
+
@@ -143,6 +157,7 @@
+
@@ -162,6 +177,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
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/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 7d419ed8f..942f5a79a 100644
--- a/src/main/java/org/amahi/anywhere/AmahiModule.java
+++ b/src/main/java/org/amahi/anywhere/AmahiModule.java
@@ -38,14 +38,17 @@
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.TvPlaybackAudioActivity;
import org.amahi.anywhere.tv.activity.TvPlaybackVideoActivity;
import org.amahi.anywhere.tv.fragment.MainTVFragment;
import org.amahi.anywhere.tv.fragment.ServerFileTvFragment;
+import org.amahi.anywhere.util.UploadManager;
import org.amahi.anywhere.tv.fragment.TvPlaybackAudioFragment;
import org.amahi.anywhere.tv.fragment.TvPlaybackVideoFragment;
@@ -79,11 +82,14 @@
ServerFileImageFragment.class,
ServerFileDownloadingFragment.class,
SettingsFragment.class,
+ UploadSettingsFragment.class,
AudioService.class,
VideoService.class,
MainTVFragment.class,
TVWebViewActivity.class,
ServerFileTvFragment.class,
+ UploadService.class,
+ UploadManager.class,
TvPlaybackVideoFragment.class,
TvPlaybackVideoActivity.class,
TvPlaybackAudioActivity.class,
diff --git a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java
index f0f0f3c3e..5ef4f488c 100644
--- a/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java
+++ b/src/main/java/org/amahi/anywhere/activity/ServerFilesActivity.java
@@ -19,13 +19,28 @@
package org.amahi.anywhere.activity;
+import android.Manifest;
import android.app.DialogFragment;
+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.NonNull;
+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.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
+import android.view.View;
import com.squareup.otto.Subscribe;
@@ -35,45 +50,71 @@
import org.amahi.anywhere.bus.FileDownloadedEvent;
import org.amahi.anywhere.bus.FileOpeningEvent;
import org.amahi.anywhere.bus.ServerFileSharingEvent;
+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.fragment.GooglePlaySearchFragment;
import org.amahi.anywhere.fragment.ServerFileDownloadingFragment;
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 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 +139,8 @@ private void setUpHomeNavigation() {
private void setUpFiles(Bundle state) {
setUpFilesTitle();
+ setUpUploadFAB();
+ setUpUploadDialog();
setUpFilesFragment();
setUpFilesState(state);
}
@@ -106,13 +149,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 +288,235 @@ 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) {
+ 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);
+ if (file.exists()) {
+ 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(0, 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 +555,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/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/adapter/FilesFilterBaseAdapter.java b/src/main/java/org/amahi/anywhere/adapter/FilesFilterBaseAdapter.java
index 8e9458b07..22be4806d 100644
--- a/src/main/java/org/amahi/anywhere/adapter/FilesFilterBaseAdapter.java
+++ b/src/main/java/org/amahi/anywhere/adapter/FilesFilterBaseAdapter.java
@@ -20,12 +20,10 @@
package org.amahi.anywhere.adapter;
import android.content.Context;
-import android.graphics.Bitmap;
import android.graphics.Color;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.AsyncTask;
-import android.support.annotation.DrawableRes;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
@@ -37,8 +35,6 @@
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
-import com.bumptech.glide.request.animation.GlideAnimation;
-import com.bumptech.glide.request.target.SimpleTarget;
import org.amahi.anywhere.R;
import org.amahi.anywhere.server.client.ServerClient;
@@ -120,6 +116,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..23f934743
--- /dev/null
+++ b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadCompleteEvent.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.bus;
+
+public class ServerFileUploadCompleteEvent implements BusEvent {
+ private int id;
+ private boolean wasUploadSuccessful;
+
+ public ServerFileUploadCompleteEvent(int id, boolean wasUploadSuccessful) {
+ this.id = id;
+ this.wasUploadSuccessful = 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
new file mode 100644
index 000000000..d183935c1
--- /dev/null
+++ b/src/main/java/org/amahi/anywhere/bus/ServerFileUploadProgressEvent.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.bus;
+
+public class ServerFileUploadProgressEvent implements BusEvent {
+ private int id;
+ private int progress;
+
+ public ServerFileUploadProgressEvent(int id, int progress) {
+ this.id = id;
+ this.progress = progress;
+ }
+
+ public int getId() {
+ return this.id;
+ }
+
+ 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/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/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/fragment/ServerFilesFragment.java b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java
index 0da43701c..a52aec2fc 100644
--- a/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java
+++ b/src/main/java/org/amahi/anywhere/fragment/ServerFilesFragment.java
@@ -20,20 +20,20 @@
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.Handler;
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;
@@ -48,6 +48,7 @@
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.TextView;
+import android.widget.Toast;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext;
@@ -64,12 +65,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;
@@ -82,19 +85,22 @@
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,
- CastStateListener
+ SwipeRefreshLayout.OnRefreshListener,
+ AdapterView.OnItemClickListener,
+ AdapterView.OnItemLongClickListener,
+ ActionMode.Callback,
+ SearchView.OnQueryTextListener,
+ FilesFilterBaseAdapter.onFilterListChange,
+ EasyPermissions.PermissionCallbacks,
+ CastStateListener
{
private SearchView searchView;
private MenuItem searchMenuItem;
@@ -102,9 +108,12 @@ public class ServerFilesFragment extends Fragment implements
private CastContext mCastContext;
private IntroductoryOverlay mIntroductoryOverlay;
private MenuItem mediaRouteMenuItem;
+ private ProgressDialog deleteProgressDialog;
+ private int deleteFilePosition;
+ private int lastCheckedFileIndex = -1;
+
- private static final class State
- {
+ private static final class State {
private State() {
}
@@ -112,10 +121,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
}
@@ -147,6 +155,8 @@ public void onActivityCreated(Bundle savedInstanceState) {
setUpCast();
setUpFiles(savedInstanceState);
+
+ setUpProgressDialog();
}
private void setUpInjections() {
@@ -199,6 +209,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);
}
@@ -256,63 +273,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());
}
@@ -427,14 +494,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;
@@ -516,10 +583,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);
}
@@ -535,7 +602,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());
}
}
@@ -546,8 +613,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()) {
@@ -588,13 +655,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) {
@@ -620,7 +688,6 @@ public boolean onOptionsItemSelected(MenuItem menuItem) {
setUpFilesContentSortSwitched();
setUpFilesContentSortIcon(menuItem);
return true;
-
default:
return super.onOptionsItemSelected(menuItem);
}
@@ -668,8 +735,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() {
@@ -679,6 +746,22 @@ private void collapseSearchView() {
}
}
+ public boolean checkForDuplicateFile(String fileName) {
+ List files;
+
+ if (!isMetadataAvailable()) {
+ files = getFilesAdapter().getItems();
+ } else {
+ files = getFilesMetadataAdapter().getItems();
+ }
+ for (ServerFile serverFile : files) {
+ if (serverFile.getName().equals(fileName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public void onResume() {
super.onResume();
@@ -730,16 +813,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/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/UploadBottomSheet.java b/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java
new file mode 100644
index 000000000..053d04f3c
--- /dev/null
+++ b/src/main/java/org/amahi/anywhere/fragment/UploadBottomSheet.java
@@ -0,0 +1,102 @@
+/*
+ * 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.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;
+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) {
+ 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
+ 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/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/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/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/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/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 740baaa95..afe1c6853 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;
@@ -28,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;
@@ -40,18 +42,30 @@
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 org.json.JSONArray;
+import org.json.JSONException;
+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;
/**
@@ -59,8 +73,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;
@@ -148,10 +161,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() {
@@ -179,18 +205,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 +241,67 @@ 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());
+ }
+
+ private MultipartBody.Part createFilePart(int id, File file) {
+ return MultipartBody.Part.createFormData("file",
+ file.getName(),
+ new ProgressRequestBody(id, 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);
+ }
+
+ 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) {
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/model/Server.java b/src/main/java/org/amahi/anywhere/server/model/Server.java
index dabd2b30d..5cb6d81ea 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,19 @@ 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(String session) {
+ this.session = session;
+ }
+
+ 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/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..25fc0354d
--- /dev/null
+++ b/src/main/java/org/amahi/anywhere/server/response/ServerFileUploadResponse.java
@@ -0,0 +1,55 @@
+/*
+ * 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 {
+ 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/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/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/Android.java b/src/main/java/org/amahi/anywhere/util/Android.java
index 9ef6abec3..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.
*/
@@ -38,6 +41,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;
}
@@ -80,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;
+ }
}
diff --git a/src/main/java/org/amahi/anywhere/util/Intents.java b/src/main/java/org/amahi/anywhere/util/Intents.java
index 3b7278c9b..9858661f5 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;
@@ -39,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.TvPlaybackAudioActivity;
@@ -59,7 +63,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() {
}
}
@@ -68,7 +72,6 @@ private static final class Uris {
static final String EMAIL = "mailto:%s?subject=%s";
static final String GOOGLE_PLAY = "market://details?id=%s";
static final String GOOGLE_PLAY_SEARCH = "market://search?q=%s";
-
private Uris() {
}
}
@@ -215,5 +218,32 @@ 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);
+ }
+
+ 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
new file mode 100644
index 000000000..b75052ddf
--- /dev/null
+++ b/src/main/java/org/amahi/anywhere/util/ProgressRequestBody.java
@@ -0,0 +1,105 @@
+/*
+ * 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 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/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 e233a987a..e6e5534b1 100644
--- a/src/main/res/layout/activity_server_files.xml
+++ b/src/main/res/layout/activity_server_files.xml
@@ -18,17 +18,11 @@
~ You should have received a copy of the GNU General Public License
~ along with Amahi. If not, see .
-->
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
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 572f75a1d..6eb63d8e2 100644
--- a/src/main/res/menu/action_bar_server_files.xml
+++ b/src/main/res/menu/action_bar_server_files.xml
@@ -17,9 +17,8 @@
~ along with Amahi. If not, see .
-->
-