public class VaporApp extends Application {

    private static String TAG = "VaporApp";

    @Inject
    Bus bus;
    @Inject
    NotificationManager notificationManager;
    @Inject
    RefCountManager refCountManager;

    VaporAppComponent vaporAppComponent;
    UiComponent uiComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        //Bad static initializers.
        SugarContext.init(this);
        Once.initialise(this);
        Hawk.init(this)
        .setEncryptionMethod(HawkBuilder.EncryptionMethod.NO_ENCRYPTION)
                .setStorage(HawkBuilder.newSqliteStorage(this))
                .setLogLevel(LogLevel.FULL)
                .build();


        vaporAppComponent = VaporAppComponent.Initializer.init(this);

        uiComponent = DaggerUiComponent.builder()
                .vaporAppComponent(vaporAppComponent)
                .build();

        vaporAppComponent.inject(this);

        registerBus();

        initStetho();

        if (BuildConfig.DEBUG) {
            //When debugging logs will go through the Android logger
            Timber.plant(new Timber.DebugTree());

            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls()
            .detectDiskReads()
            .detectDiskWrites()
            .penaltyLog()
            .build());

            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
            .detectActivityLeaks()
            .penaltyLog()
            .build());
        }

        Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this));
    }

    private void registerBus() {
        bus.register(this);
    }

    private void initStetho() {
        Stetho.initialize(
                Stetho.newInitializerBuilder(this)
                        .enableDumpapp(
                                Stetho.defaultDumperPluginsProvider(this))
                        .enableWebKitInspector(
                                Stetho.defaultInspectorModulesProvider(this))
                        .build());
    }


    public VaporApp() {
    }

    public static UiComponent uiComponent(Context context) {
        return ((VaporApp) context.getApplicationContext()).uiComponent;
    }

    public static VaporAppComponent appComponent(Context context) {
        return ((VaporApp) context.getApplicationContext()).vaporAppComponent;
    }

    public static void recreateUiComponent(Context context) {
        ((VaporApp) context.getApplicationContext()).uiComponent = DaggerUiComponent.builder()
                .vaporAppComponent(((VaporApp) context.getApplicationContext()).vaporAppComponent)
                .build();
    }

    public static void recreateVaporComponent(Context context) {
        ((VaporApp) context.getApplicationContext()).vaporAppComponent =
                VaporAppComponent.Initializer.init((VaporApp)context.getApplicationContext());
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        Glide.get(this).clearMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        Glide.get(this).trimMemory(level);
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        SugarContext.terminate();
        bus.unregister(this);

        for (Integer notificationId : refCountManager.getNotificationIds()) {
            notificationManager.cancel(notificationId);
            Timber.i("CLEAN UP from onTerminate with id=%s", notificationId);
        }
    }

    @Subscribe
    public void onLogoutEvent(LogoutEvent event) {
        // Clear the cache of the Http client before we create a new one.
        try {
            vaporAppComponent.client().getCache().evictAll();
        } catch (IOException e) {
            e.printStackTrace();
        }
        VaporApp.recreateVaporComponent(this);
        VaporApp.recreateUiComponent(this);
        Glide.get(VaporApp.this).clearMemory();
        Single.just(new Object()).subscribeOn(Schedulers.io()).subscribe(new Action1<Object>() {
            @Override
            public void call(Object o) {
                Glide.get(VaporApp.this).clearDiskCache();
            }
        });


        vaporAppComponent.inject(this);
        registerBus();


    }

    /*http://stackoverflow.com/questions/4028742/how-to-clear-a-notification-if-activity-crashes*/
    public class CrashHandler implements Thread.UncaughtExceptionHandler {

        private Thread.UncaughtExceptionHandler defaultUEH;
        private NotificationManager notificationManager;

        public CrashHandler(Context context) {
            this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
            notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        }

        public void uncaughtException(Thread t, Throwable e) {
            for (Integer notificationId : refCountManager.getNotificationIds()) {
                notificationManager.cancel(notificationId);
                Timber.i("CLEAN UP from uncaughtException with id=%s", notificationId);
            }
            notificationManager = null;
            defaultUEH.uncaughtException(t, e);
        }
    }
} public class UploadEvent {
    private Uri uri;
    private CloudAppItem item;

    public UploadEvent(Uri uri, CloudAppItem item) {
        this.uri = uri;
        this.item = item;
    }

    public UploadEvent() {
    }

    @Nullable
    public Uri getUri() {
        return uri;
    }

    public CloudAppItem getItem() {
        return item;
    }
} public class UploadFailedEvent {
    private final Uri uri;

    public UploadFailedEvent(Uri uri) {
        this.uri = uri;
    }

    public Uri getUri() {
        return uri;
    }
} public abstract class CloudAppRequestBody extends RequestBody {
    public abstract String getFileName();
    public abstract long contentLength();
    public String getFileSize() {
        return String.valueOf(contentLength());
    }
} public class DataManager {

    private final CloudAppService cloudAppService;
    private final Bus bus;
    private final UserManager userManager;
    private final int SERVER_ITEM_LIMIT = 40;
    private final int MAX_ITEM_LIMIT = 1500;
    private final OkHttpClient client;
    private boolean isSyncingAllItems;

    public final static String SYNC_ALL_ITEMS = DataManager.class.getPackage().getName() + ".SYNC_ALL_ITEMS";

    public DataManager(CloudAppService cloudAppService, UserManager userManager, Bus bus, OkHttpClient client) {
        this.cloudAppService = cloudAppService;
        this.bus = bus;
        this.userManager = userManager;
        this.client = client;
        bus.register(this);
    }

    public void syncAllItems(final boolean notify) {
        if (!isSyncingAllItems) {
            cloudAppService.getAccountStats()
                    .flatMap(new Func1<AccountStatsModel, Observable<List<CloudAppItem>>>() {
                        @Override
                        public Observable<List<CloudAppItem>> call(final AccountStatsModel accountStatsModel) {
                            return Observable.create(new Observable.OnSubscribe<Integer>() {
                                @Override
                                public void call(Subscriber<? super Integer> subscriber) {
                                    if (!subscriber.isUnsubscribed()) {
                                        for (int i = 1; i <= Math.ceil((double) accountStatsModel.getItems() / (double) SERVER_ITEM_LIMIT) + 1
                                                && i < Math.ceil(MAX_ITEM_LIMIT / SERVER_ITEM_LIMIT); i++) {
                                            subscriber.onNext(i);
                                        }
                                        subscriber.onCompleted();
                                    }
                                }
                            })
                                    .subscribeOn(Schedulers.io())
                                    .flatMap(new Func1<Integer, Observable<List<CloudAppItem>>>() {
                                        @Override
                                        public Observable<List<CloudAppItem>> call(Integer integer) {
                                            return getListFromServer(makeListParams(integer, ItemType.ALL, false, SERVER_ITEM_LIMIT));
                                        }
                                    }).subscribeOn(Schedulers.io())
                                    .flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                                        @Override
                                        public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                                            SugarRecord.updateInTx(cloudAppItems);
                                            return Observable.just(cloudAppItems);
                                        }
                                    });

                        }
                    })
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .doOnTerminate(new Action0() {
                        @Override
                        public void call() {
                            isSyncingAllItems = false;
                            if (notify) {
                                bus.post(new DatabaseUpdateEvent());
                            }
                        }
                    })
                    .doOnSubscribe(new Action0() {
                        @Override
                        public void call() {
                            isSyncingAllItems = true;
                        }
                    })
                    .subscribe(new Action1<List<CloudAppItem>>() {
                        @Override
                        public void call(List<CloudAppItem> cloudAppItems) {
                            Timber.i("Synced all items. Size: %s", cloudAppItems.size());
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            Timber.e(throwable, "Error syncing all items");
                        }
                    });

            getTrashItems(ItemType.ALL, true, new DataCursor())
                    .subscribe(new Action1<List<CloudAppItem>>() {
                        @Override
                        public void call(List<CloudAppItem> cloudAppItems) {
                            Timber.i("Synced all deleted items. Size: %s", cloudAppItems.size());
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            Timber.e(throwable, "Error syncing all deleted items");
                        }
                    });
        }
    }

    @NonNull
    private Func1<List<CloudAppItem>, Observable<CloudAppItem>> reduceList() {
        return new Func1<List<CloudAppItem>, Observable<CloudAppItem>>() {
            @Override
            public Observable<CloudAppItem> call(List<CloudAppItem> cloudAppItems) {
                return Observable.from(cloudAppItems);
            }
        };
    }

    @NonNull
    private Func1<CloudAppItem, CloudAppItem> saveToDb() {
        return new Func1<CloudAppItem, CloudAppItem>() {
            @Override
            public CloudAppItem call(CloudAppItem cloudAppItem) {
                SugarRecord.updateInTx(cloudAppItem);
                Timber.i("Inserting or update item: %s", cloudAppItem.getItemId());
                return cloudAppItem;
            }
        };
    }

    private void deleteLocalItem(CloudAppItem cloudAppItem) {
        SugarRecord.delete(cloudAppItem);
        Timber.i("Deleting item: %s", cloudAppItem.getItemId());
    }

    public void purgeDeletedItems() {
        long expired = DateTime.now().minusDays(7).getMillis();
        Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                "DELETE FROM CLOUD_APP_ITEM WHERE deleted_at <> -1 AND deleted_at < ?",
                String.valueOf(expired)))
                .subscribe(new Action1<List<CloudAppItem>>() {
                    @Override
                    public void call(List<CloudAppItem> cloudAppItems) {
                        Timber.i("Deleting items: %s", cloudAppItems.size());
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        Timber.e(throwable, "Error deleting stale items.");
                    }
                });
    }

    public Observable<CloudAppItem> deleteCloudItem(final CloudAppItem cloudAppItem) {
        return cloudAppService.deleteItem(String.valueOf(cloudAppItem.getItemId()))
                .map(convertItemModel())
                .doOnNext(new Action1<CloudAppItem>() {
                    @Override
                    public void call(CloudAppItem item) {
                        deleteLocalItem(cloudAppItem);
                    }
                });
    }

    public Observable<CloudAppItem> renameCloudItem(final CloudAppItem cloudAppItem, String newName) {
        CloudAppJsonItem jsonItem = new CloudAppJsonItem();
        CloudAppJsonItem.Item item = jsonItem.item = new CloudAppJsonItem.Item();
        item.name = newName;

        return cloudAppService.renameItem(String.valueOf(cloudAppItem.getItemId()), jsonItem)
                .map(convertItemModel())
                .map(saveToDb());
    }

    public Observable<CloudAppItem> bookmarkItem(final String name, String url) {
        CloudAppJsonItem jsonItem = new CloudAppJsonItem();
        CloudAppJsonItem.Item item = jsonItem.item = new CloudAppJsonItem.Item();
        item.name = name;
        item.redirectUrl = url;

        return cloudAppService.bookmarkLink(jsonItem)
                .map(convertItemModel())
                .map(saveToDb())
                .retryWhen(new RxUtils.RetryWithDelay(2, 2000));
    }

    public Observable<CloudAppItem> upload(final CloudAppRequestBody requestBody) {
        return cloudAppService.newUpload(makeQueryMap(requestBody.getFileName(), String.valueOf(requestBody.contentLength())))
                .flatMap(new Func1<UploadModel, Observable<CloudAppItem>>() {
                    @Override
                    public Observable<CloudAppItem> call(final UploadModel uploadModel) {
                        if (uploadModel.getParams() == null) {
                            return Observable.error(new UploadLimitException("Daily upload limit reached"));
                        }
                        if (requestBody.contentLength() > uploadModel.getMax_upload_size()) {
                            return Observable.error(new FileToLargeException("File too large for you current plan"));
                        }
                        return cloudAppService.uploadFile(makeMultipartParams(uploadModel, requestBody))
                                .map(convertItemModel())
                                .map(saveToDb())
                                .retryWhen(new RxUtils.RetryWithDelay(4, 2000));

                    }
                });
    }

    private Map<String, String> makeQueryMap(String name, String size) {
        Map<String, String> params = new LinkedHashMap<>();
        params.put("name", name);
        params.put("file_size", size);
        return params;
    }

    private Observable<List<CloudAppItem>> getAllItems(final ItemType type, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        if (type == ItemType.ALL) {
            return Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE deleted_at = -1 ORDER BY ITEM_ID DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"),
                            String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class, query,
                            String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        return Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
            @Override
            public Observable<List<CloudAppItem>> call() {
                String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND deleted_at = -1 ORDER BY ITEM_ID DESC LIMIT ? OFFSET ?";
                Timber.d(query.replace("?", "%s"),
                        type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class, query,
                                type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
            }
        });
    }

    public Observable<List<CloudAppItem>> getPopularItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable;

        if (type == ItemType.ALL) {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE deleted_at = -1 ORDER BY VIEW_COUNTER DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        } else {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND deleted_at = -1 ORDER BY VIEW_COUNTER DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        if (refresh) {
            return refreshPage(1, false)
                    .flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    public Observable<List<CloudAppItem>> getFavoriteItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable;

        if (type == ItemType.ALL) {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE FAVORITE = 1 AND deleted_at = -1 ORDER BY ITEM_ID DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }
            });
        } else {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND FAVORITE = 1 AND deleted_at = -1 ORDER BY ITEM_ID, VIEW_COUNTER DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        if (refresh) {
            return refreshPage(1, false).flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    public Observable<List<CloudAppItem>> getTrashItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable;
        if (type == ItemType.ALL) {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE DELETED_AT <> -1 ORDER BY DELETED_AT DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return  Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }
            });
        } else {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND DELETED_AT <> -1 ORDER BY DELETED_AT DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        if (refresh) {
            return refreshPage(1, true).flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    public Observable<List<CloudAppItem>> getAllItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable = getAllItems(type, cursor);
        if (refresh) {
            return refreshPage(1, false).flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    @NonNull
    private Observable<List<CloudAppItem>> refreshPage(int page, boolean deleted) {
        return getListFromServer(makeListParams(page, ItemType.ALL, deleted, SERVER_ITEM_LIMIT))
                .flatMap(reduceList())
                .map(saveToDb())
                .toList();
    }

    private Observable<List<CloudAppItem>> getListFromServer(Map<String, String> options) {
        return cloudAppService.listItems(options)
                .retryWhen(new RxUtils.RetryWithDelay(2, 2000))
                .subscribeOn(Schedulers.io())
                .flatMap(new Func1<List<ItemModel>, Observable<List<CloudAppItem>>>() {
                    @Override
                    public Observable<List<CloudAppItem>> call(List<ItemModel> itemModels) {
                        return Observable.from(itemModels)
                                .map(convertItemModel()).toList();
                    }
                });
    }

    @NonNull
    private Func1<ItemModel, CloudAppItem> convertItemModel() {
        return new Func1<ItemModel, CloudAppItem>() {
            @Override
            public CloudAppItem call(ItemModel itemModel) {
                return new CloudAppItem(itemModel);
            }
        };
    }

    private Map<String, String> makeListParams(int page, ItemType type, boolean deleted, int pageSize) {
        Map<String, String> params = new HashMap<>();
        params.put("page", String.valueOf(page));
        params.put("per_page", String.valueOf(pageSize));
        if (type != ItemType.ALL) {
            params.put("type", type.toString().toLowerCase());
        }
        params.put("deleted", String.valueOf(deleted));

        return params;
    }

    private Map<String, RequestBody> makeMultipartParams(UploadModel uploadModel, CloudAppRequestBody body) {
        String filename = "file\"; filename=\"" + body.getFileName();
        Map<String, RequestBody> params = new LinkedHashMap<>();
        params.put("AWSAccessKeyId", createStringBody(uploadModel.getParams().getAWSAccessKeyId()));
        params.put("key", createStringBody(uploadModel.getParams().getKey()));
        params.put("acl", createStringBody(uploadModel.getParams().getAcl()));
        params.put("success_action_redirect", createStringBody(uploadModel.getParams().getSuccess_action_redirect()));
        params.put("signature", createStringBody(uploadModel.getParams().getSignature()));
        params.put("policy",createStringBody( uploadModel.getParams().getPolicy()));
        params.put(filename, body);
        return params;
    }

    private RequestBody createStringBody(String string) {
        return RequestBody.create(MediaType.parse("text/plain"), string);
    }

    @Subscribe
    public void onLogoutEvent(LogoutEvent event) {
        List<CloudAppItem> cloudAppItems = SugarRecord.listAll(CloudAppItem.class);
        SugarRecord.deleteInTx(cloudAppItems);
        Once.clearAll();
        CloudAppItem.executeQuery("DELETE FROM sqlite_sequence WHERE NAME = 'CLOUD_APP_ITEM'");
        CloudAppItem.executeQuery("VACUUM");
    }

    public boolean isSyncingAllItems() {
        return isSyncingAllItems;
    }

    public static class DataCursor implements Parcelable {
        public final int ITEM_LIST_LIMIT = 100;
        int offset = 0;
        String owner = "Dummy";

        public DataCursor(String owner) {
            this.owner = owner;
        }

        public int getLimit() {
            return ITEM_LIST_LIMIT;
        }

        public int getOffset() {
            return offset;
        }

        public void setOffset(int offset) {
            Timber.i("%s Cursor was %s now %s", owner,  this.offset, offset);
            this.offset = offset;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(this.offset);
        }

        public DataCursor() {
        }

        protected DataCursor(Parcel in) {
            this.offset = in.readInt();
        }

        public static final Creator<DataCursor> CREATOR = new Creator<DataCursor>() {
            public DataCursor createFromParcel(Parcel source) {
                return new DataCursor(source);
            }

            public DataCursor[] newArray(int size) {
                return new DataCursor[size];
            }
        };
    }
} public interface ProgressListener {

    /**
     * When progress is reported
     * @param current The current progress
     * @param max The max progress amount
     */
    void onProgress(final long current, final long max);
} public class ProgressNotification implements Action1<Long> {
    private final Long fileSize;
    private final NotificationCompat.Builder builder;
    private final NotificationManager notificationManager;
    private final int notificationId;

    public ProgressNotification(Long fileSize, NotificationCompat.Builder builder, NotificationManager notificationManager, int notificationId) {
        this.fileSize = fileSize;
        this.builder = builder;
        this.notificationManager = notificationManager;
        this.notificationId = notificationId;
    }

    @Override
    public void call(Long current) {
        if (fileSize != 0) {
            double ratio = 1 / ((double) fileSize / current);
            double percentage = ratio * 100;
            int progress = (int) Math.floor(percentage);
            builder.setProgress(100, progress, false);
        }
        builder.setContentInfo(VaporUtils.humanReadableByteCount(current, true));
        notificationManager.notify(notificationId, builder.build());

    }
} public class ProgressiveFileRequestBody extends CloudAppRequestBody {

    private static final int DEFAULT_BUFFER_SIZE = 4096;

    private final ProgressListener listener;
    private final File file;

    public ProgressiveFileRequestBody(final File file, ProgressListener listener) {
        this.file = file;
        this.listener = listener;
    }

    @Override
    public long contentLength() {
        return file.length();
    }

    @Override
    public MediaType contentType() {
        return MediaType.parse(getMimeType(file.getAbsolutePath()));
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        long fileLength = contentLength();
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        FileInputStream in = new FileInputStream(file);
        long total = 0;
        try {
            int read;
            while ((read = in.read(buffer)) != -1) {
                this.listener.onProgress(total, fileLength);
                total += read;
                sink.write(buffer, 0, read);
            }
        } finally {
            in.close();
        }
    }

    private static String getMimeType(String url) {
        String type = null;
        String extension = MimeTypeMap.getFileExtensionFromUrl(url);
        if (extension != null) {
            type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
        }
        if (type == null) {
            return "application/octet-stream";
        }
        return type;
    }

    @Override
    public String getFileName() {
        return file.getName();
    }
} public class ProgressiveStringRequestBody extends CloudAppRequestBody {

    private static final int DEFAULT_BUFFER_SIZE = 4096;

    private final ProgressListener listener;
    private final InputStream inputStream;
    private final byte[] bytes;
    private final String name;

    public ProgressiveStringRequestBody(final String string, String name, ProgressListener listener) {
        Charset charset = Util.UTF_8;
        this.bytes = string.getBytes(charset);
        this.name = name == null? string.substring(0, string.length() < 18? string.length(): 18).trim() + ".txt" : name; //Sorry
        this.inputStream = new ByteArrayInputStream(bytes);
        this.listener = listener;
    }

    @Override
    public long contentLength() {
        return bytes.length;
    }

    @Override
    public MediaType contentType() {
        return MediaType.parse("text/plain");
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        long fileLength = contentLength();
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        long total = 0;
        try {
            int read;
            while ((read = inputStream.read(buffer)) != -1) {
                this.listener.onProgress(total, fileLength);
                total += read;
                sink.write(buffer, 0, read);
            }
        } finally {
            inputStream.close();
        }
    }

    @Override
    public String getFileName() {
        return name;
    }
} public class RefCountManager {

    private final Context context;
    private final List<Uri> uris = new ArrayList<>();
    private final List<Integer> notificationIds = new ArrayList<>();

    public RefCountManager(Context context) {
        this.context = context;
    }

    public void addNotificationId(int startId) {
        notificationIds.add(startId);
    }

    public void removeNotificationId(int startId) {
        notificationIds.remove(Integer.valueOf(startId));
    }

    public List<Integer> getNotificationIds() {
        return notificationIds;
    }

    public void addUri(Uri uri) {
        uris.add(uri);
    }

    private void cleanUpUri(final Uri uri) {
        // If the list of Uris contains an instance of the parameter uri, then create a file out of it
        // then delete it. Use of RxJava is completely unnecessary.
        Observable.defer(new Func0<Observable<Boolean>>() {
            @Override
            public Observable<Boolean> call() {
                return Observable.just(uri)
                        .filter(new Func1<Uri, Boolean>() {
                            @Override
                            public Boolean call(Uri uri) {
                                return uris.contains(uri);
                            }
                        })
                        .flatMap(new Func1<Uri, Observable<File>>() {
                            @Override
                            public Observable<File> call(Uri uri) {
                                return Observable.just(FileUtils.getFile(context, uri));
                            }
                        })
                        .map(new Func1<File, Boolean>() {
                            @Override
                            public Boolean call(File file) {
                                return file.delete();
                            }
                        });
            }
        })
                .subscribeOn(Schedulers.io())
                .subscribe(new Observer<Boolean>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                Timber.e(e, "Error while attempting to delete a file created during upload");
            }

            @Override
            public void onNext(Boolean fileDeleted) {
                if (fileDeleted) {
                    Timber.i("Clean up successful");
                } else {
                    Timber.i("Clean up failed");
                }
            }
        });
    }

    @Subscribe
    public void onUploadEvent(UploadEvent uploadEvent) {
        if (uploadEvent.getUri() != null) {
            cleanUpUri(uploadEvent.getUri());
        }
    }

    @Subscribe
    public void onUploadFailed(UploadFailedEvent uploadEvent) {
        if (uploadEvent.getUri() != null) {
            cleanUpUri(uploadEvent.getUri());
        }
    }
} public class FileToLargeException extends CloudAppException {
    public FileToLargeException(String detailMessage) {
        super(detailMessage);
    }
} public class UploadLimitException extends CloudAppException {
    public UploadLimitException(String detailMessage) {
        super(detailMessage);
    }
} public class CloudAppItem extends SugarRecord implements Comparable<CloudAppItem>,Parcelable {

    @Unique
    private long itemId;
    private String href;
    private String name;
    private boolean isPrivate;
    private boolean isSubscribed;
    private String contentUrl;
    private String itemType;
    private long viewCounter;
    private String icon;
    private String url;
    private String remoteUrl;
    private String thumbnailUrl;
    private String downloadUrl;
    private String source;
    boolean favorite;
    String ownerId;
    long contentLength;
    private String createdAt;
    private String updatedAt;
    private String deletedAt;
    private String lastViewedAt;

    public CloudAppItem(ItemModel itemModel) {
        this.itemId = itemModel.id;
        this.href = itemModel.href;
        this.name = itemModel.name;
        this.isPrivate = itemModel.isPrivate;
        this.isSubscribed = itemModel.isSubscribed;
        this.contentUrl = itemModel.contentUrl;
        this.itemType = itemModel.itemType;
        this.viewCounter = itemModel.viewCounter;
        this.icon = itemModel.icon;
        this.url = itemModel.url;
        this.remoteUrl = itemModel.remoteUrl;
        this.thumbnailUrl = itemModel.thumbnailUrl;
        this.downloadUrl = itemModel.downloadUrl;
        this.source = itemModel.source;
        this.favorite = itemModel.favorite;
        this.ownerId = itemModel.ownerId;
        this.contentLength = itemModel.contentLength;
        this.createdAt = String.valueOf(formatDate(itemModel.createdAt));
        this.updatedAt = String.valueOf(formatDate(itemModel.updatedAt));
        this.deletedAt = String.valueOf(formatDate(itemModel.deletedAt));
        this.lastViewedAt = String.valueOf(formatDate(itemModel.lastViewedAt));
    }

    public CloudAppItem() {
    }

    public long getItemId() {
        return itemId;
    }

    public String getDownloadUrl() {
        return downloadUrl;
    }

    public String getHref() {
        return href;
    }

    
    public String getName() {
        return name;
    }

    
    public boolean isPrivate() {
        return isPrivate;
    }

    
    public boolean isSubscribed() {
        return isSubscribed;
    }

    public boolean isTrashed() {
        return !deletedAt.equals(String.valueOf(-1));
    }

    public String getUrl() {
        return url == null? remoteUrl : url;
    }

    
    public String getContentUrl() {
        return contentUrl;
    }

    
    public ItemType getItemType() {
        try {
            return ItemType.valueOf(itemType.toUpperCase());
        } catch (IllegalArgumentException e) {
            return ItemType.UNKNOWN;
        }
    }

    
    public long getViewCounter() {
        return viewCounter;
    }

    
    public String getIcon() {
        return icon;
    }

    
    public String getRemoteUrl() {
        return remoteUrl;
    }

    
    public String getSource() {
        return source;
    }

    
    public String getCreatedAt() {
        return createdAt;
    }

    
    public String getUpdatedAt() {
        return updatedAt;
    }

    
    public String getDeletedAt() {
        return deletedAt;
    }

    
    public String getLastViewedAt() {
        return lastViewedAt;
    }

    
    public String getThumbnailUrl() {
        return thumbnailUrl;
    }

    public long getContentLength() {
        return contentLength;
    }

    public String getFormattedCreatedAt(Context context)  {

        return formatDateTime(context, Long.parseLong(createdAt), FORMAT_SHOW_TIME|FORMAT_SHOW_YEAR|FORMAT_SHOW_DATE);
    }

    
    public String getFormattedUpdatedAt()  {
        try {
            return updatedAt ==null?null:DateFormat.getDateInstance()
                    .format(CloudAppUtils.format.parse(updatedAt));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    
    public String getFormattedDeletedAt() {
        try {
            return deletedAt ==null?null:DateFormat.getDateInstance()
                    .format(CloudAppUtils.format.parse(deletedAt));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    public boolean isFavorite() {
        return favorite;
    }

    public String getOwnerId() {
        return ownerId;
    }

    @Override
    public int compareTo(CloudAppItem another) {
        CloudAppItem lhs = this;
        if (another != null) {
            if (lhs.equals(another)) return 0;
            if (lhs.getItemId() < another.getItemId()) return 1;
            if (lhs.getItemId() < another.getItemId()) return -1;
        }
        return -1;
    }

    public enum ItemType {
        ALL, DELETED, AUDIO, BOOKMARK, IMAGE, UNKNOWN, VIDEO, ARCHIVE, TEXT
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setDeletedAt(String deletedAt) {
        this.deletedAt = deletedAt;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        CloudAppItem item = (CloudAppItem) o;

        return itemId == item.itemId;

    }

    public int hash() {
        int result = href.hashCode();
        result = 31 * result + createdAt.hashCode();
        result = 31 * result + updatedAt.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "CloudAppItem{" +
                "name='" + name + '\'' +
                ", href='" + href + '\'' +
                ", itemId=" + itemId +
                '}';
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(this.itemId);
        dest.writeString(this.href);
        dest.writeString(this.name);
        dest.writeByte(isPrivate ? (byte) 1 : (byte) 0);
        dest.writeByte(isSubscribed ? (byte) 1 : (byte) 0);
        dest.writeString(this.contentUrl);
        dest.writeString(this.itemType);
        dest.writeLong(this.viewCounter);
        dest.writeString(this.icon);
        dest.writeString(this.url);
        dest.writeString(this.remoteUrl);
        dest.writeString(this.thumbnailUrl);
        dest.writeString(this.downloadUrl);
        dest.writeString(this.source);
        dest.writeByte(favorite ? (byte) 1 : (byte) 0);
        dest.writeString(this.ownerId);
        dest.writeLong(this.contentLength);
        dest.writeString(this.createdAt);
        dest.writeString(this.updatedAt);
        dest.writeString(this.deletedAt);
        dest.writeString(this.lastViewedAt);
    }

    protected CloudAppItem(Parcel in) {
        this.itemId = in.readLong();
        this.href = in.readString();
        this.name = in.readString();
        this.isPrivate = in.readByte() != 0;
        this.isSubscribed = in.readByte() != 0;
        this.contentUrl = in.readString();
        this.itemType = in.readString();
        this.viewCounter = in.readLong();
        this.icon = in.readString();
        this.url = in.readString();
        this.remoteUrl = in.readString();
        this.thumbnailUrl = in.readString();
        this.downloadUrl = in.readString();
        this.source = in.readString();
        this.favorite = in.readByte() != 0;
        this.ownerId = in.readString();
        this.contentLength = in.readLong();
        this.createdAt = in.readString();
        this.updatedAt = in.readString();
        this.deletedAt = in.readString();
        this.lastViewedAt = in.readString();
    }

    public static final Parcelable.Creator<CloudAppItem> CREATOR = new Parcelable.Creator<CloudAppItem>() {
        public CloudAppItem createFromParcel(Parcel source) {
            return new CloudAppItem(source);
        }

        public CloudAppItem[] newArray(int size) {
            return new CloudAppItem[size];
        }
    };
} public class FileUtils {
    private FileUtils() {} //private constructor to enforce Singleton pattern

    /** TAG for log messages. */
    static final String TAG = "FileUtils";
    private static final boolean DEBUG = true; // Set to true to enable logging

    public static final String MIME_TYPE_AUDIO = "audio/*";
    public static final String MIME_TYPE_TEXT = "text/*";
    public static final String MIME_TYPE_IMAGE = "image/*";
    public static final String MIME_TYPE_VIDEO = "video/*";
    public static final String MIME_TYPE_APP = "application/*";

    public static final String HIDDEN_PREFIX = ".";

    /**
     * Gets the extension of a file name, like ".png" or ".jpg".
     *
     * @param uri
     * @return Extension including the dot("."); "" if there is no extension;
     *         null if uri was null.
     */
    public static String getExtension(String uri) {
        if (uri == null) {
            return null;
        }

        int dot = uri.lastIndexOf(".");
        if (dot >= 0) {
            return uri.substring(dot);
        } else {
            // No extension.
            return "";
        }
    }

    /**
     * @return Whether the URI is a local one.
     */
    public static boolean isLocal(String url) {
        return url != null && !url.startsWith("http://") && !url.startsWith("https://");
    }

    /**
     * @return True if Uri is a MediaStore Uri.
     * @author paulburke
     */
    public static boolean isMediaUri(Uri uri) {
        return "media".equalsIgnoreCase(uri.getAuthority());
    }

    /**
     * Convert File into Uri.
     *
     * @param file
     * @return uri
     */
    public static Uri getUri(File file) {
        if (file != null) {
            return Uri.fromFile(file);
        }
        return null;
    }

    /**
     * Returns the path only (without file name).
     *
     * @param file
     * @return
     */
    public static File getPathWithoutFilename(File file) {
        if (file != null) {
            if (file.isDirectory()) {
                // no file to be split off. Return everything
                return file;
            } else {
                String filename = file.getName();
                String filepath = file.getAbsolutePath();

                // Construct path without file name.
                String pathwithoutname = filepath.substring(0,
                        filepath.length() - filename.length());
                if (pathwithoutname.endsWith("/")) {
                    pathwithoutname = pathwithoutname.substring(0, pathwithoutname.length() - 1);
                }
                return new File(pathwithoutname);
            }
        }
        return null;
    }

    /**
     * @return The MIME type for the given file.
     */
    public static String getMimeType(File file) {

        String extension = getExtension(file.getName());

        if (extension.length() > 0)
            return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));

        return "application/octet-stream";
    }

    public static String getMimeType(String url) {
        String type = null;
        String extension = MimeTypeMap.getFileExtensionFromUrl(url);
        if (extension != null) {
            type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
        }
        if (type == null) {
            return "application/octet-stream";
        }
        return type;
    }
    /**
     * @return The MIME type for the give Uri.
     */
    public static String getMimeType(Context context, Uri uri) {
        File file = new File(getPath(context, uri));
        return getMimeType(file);
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is {@link --}.
     * @author paulburke
     */
    public static boolean isLocalStorageDocument(Uri uri) {
        return Objects.equals("com.ianhanniballake.localstorage.documents", uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     * @author paulburke
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     * @author paulburke
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     * @author paulburke
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is Google Photos.
     */
    public static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority()) ||
                "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority());
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @param selection (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     * @author paulburke
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
                                       String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                if (DEBUG)
                    DatabaseUtils.dumpCursor(cursor);

                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }

    /**
     * Get a file path from a Uri. This will get the the path for Storage Access
     * Framework Documents, as well as the _data field for the MediaStore and
     * other file-based ContentProviders.<br>
     * <br>
     * Callers should check whether the path is local before assuming it
     * represents a local file.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @see #isLocal(String)
     * @see #getFile(Context, Uri)
     * @author paulburke
     */
    public static String getPath(final Context context, final Uri uri) {

        if (DEBUG)
            Log.d(TAG + " File -",
                    "Authority: " + uri.getAuthority() +
                            ", Fragment: " + uri.getFragment() +
                            ", Port: " + uri.getPort() +
                            ", Query: " + uri.getQuery() +
                            ", Scheme: " + uri.getScheme() +
                            ", Host: " + uri.getHost() +
                            ", Segments: " + uri.getPathSegments().toString()
            );

        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // LocalStorageProvider
            if (isLocalStorageDocument(uri)) {
                // The path is the id
                return DocumentsContract.getDocumentId(uri);
            }
            // ExternalStorageProvider
            else if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }

                // TODO handle non-primary volumes
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[] {
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {

            // Return the remote address
            if (isGooglePhotosUri(uri))
                return uri.getLastPathSegment();

            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    /**
     * Convert Uri into File, if possible.
     *
     * @return file A local file that the Uri was pointing to, or null if the
     *         Uri is unsupported or pointed to a remote resource.
     * @see #getPath(Context, Uri)
     * @author paulburke
     */
    public static File getFile(Context context, Uri uri) {
        if (uri != null) {
            String path = getPath(context, uri);
            if (path != null && isLocal(path)) {
                return new File(path);
            }
        }
        return null;
    }

    /**
     * Get the file size in a human-readable string.
     *
     * @param size
     * @return
     * @author paulburke
     */
    public static String getReadableFileSize(int size) {
        final int BYTES_IN_KILOBYTES = 1024;
        final DecimalFormat dec = new DecimalFormat("###.#");
        final String KILOBYTES = " KB";
        final String MEGABYTES = " MB";
        final String GIGABYTES = " GB";
        float fileSize = 0;
        String suffix = KILOBYTES;

        if (size > BYTES_IN_KILOBYTES) {
            fileSize = size / BYTES_IN_KILOBYTES;
            if (fileSize > BYTES_IN_KILOBYTES) {
                fileSize = fileSize / BYTES_IN_KILOBYTES;
                if (fileSize > BYTES_IN_KILOBYTES) {
                    fileSize = fileSize / BYTES_IN_KILOBYTES;
                    suffix = GIGABYTES;
                } else {
                    suffix = MEGABYTES;
                }
            }
        }
        return String.valueOf(dec.format(fileSize) + suffix);
    }

    /**
     * Attempt to retrieve the thumbnail of given File from the MediaStore. This
     * should not be called on the UI thread.
     *
     * @param context
     * @param file
     * @return
     * @author paulburke
     */
    public static Bitmap getThumbnail(Context context, File file) {
        return getThumbnail(context, getUri(file), getMimeType(file));
    }

    /**
     * Attempt to retrieve the thumbnail of given Uri from the MediaStore. This
     * should not be called on the UI thread.
     *
     * @param context
     * @param uri
     * @return
     * @author paulburke
     */
    public static Bitmap getThumbnail(Context context, Uri uri) {
        return getThumbnail(context, uri, getMimeType(context, uri));
    }

    /**
     * Attempt to retrieve the thumbnail of given Uri from the MediaStore. This
     * should not be called on the UI thread.
     *
     * @param context
     * @param uri
     * @param mimeType
     * @return
     * @author paulburke
     */
    public static Bitmap getThumbnail(Context context, Uri uri, String mimeType) {
        if (DEBUG)
            Log.d(TAG, "Attempting to get thumbnail");

        if (!isMediaUri(uri)) {
            Log.e(TAG, "You can only retrieve thumbnails for images and videos.");
            return null;
        }

        Bitmap bm = null;
        if (uri != null) {
            final ContentResolver resolver = context.getContentResolver();
            Cursor cursor = null;
            try {
                cursor = resolver.query(uri, null, null, null, null);
                if (cursor.moveToFirst()) {
                    final int id = cursor.getInt(0);
                    if (DEBUG)
                        Log.d(TAG, "Got thumb ID: " + id);

                    if (mimeType.contains("video")) {
                        bm = MediaStore.Video.Thumbnails.getThumbnail(
                                resolver,
                                id,
                                MediaStore.Video.Thumbnails.MINI_KIND,
                                null);
                    }
                    else if (mimeType.contains(FileUtils.MIME_TYPE_IMAGE)) {
                        bm = MediaStore.Images.Thumbnails.getThumbnail(
                                resolver,
                                id,
                                MediaStore.Images.Thumbnails.MINI_KIND,
                                null);
                    }
                }
            } catch (Exception e) {
                if (DEBUG)
                    Log.e(TAG, "getThumbnail", e);
            } finally {
                if (cursor != null)
                    cursor.close();
            }
        }
        return bm;
    }

    /**
     * File and folder comparator. TODO Expose sorting option method
     *
     * @author paulburke
     */
    public static Comparator<File> sComparator = new Comparator<File>() {
        @Override
        public int compare(File f1, File f2) {
            // Sort alphabetically by lower case, which is much cleaner
            return f1.getName().toLowerCase().compareTo(
                    f2.getName().toLowerCase());
        }
    };

    /**
     * File (not directories) filter.
     *
     * @author paulburke
     */
    public static FileFilter sFileFilter = new FileFilter() {
        @Override
        public boolean accept(File file) {
            final String fileName = file.getName();
            // Return files only (not directories) and skip hidden files
            return file.isFile() && !fileName.startsWith(HIDDEN_PREFIX);
        }
    };

    /**
     * Folder (directories) filter.
     *
     * @author paulburke
     */
    public static FileFilter sDirFilter = new FileFilter() {
        @Override
        public boolean accept(File file) {
            final String fileName = file.getName();
            // Return directories only and skip hidden directories
            return file.isDirectory() && !fileName.startsWith(HIDDEN_PREFIX);
        }
    };

    /**
     * Get the Intent for selecting content to be used in an Intent Chooser.
     *
     * @return The intent for opening a file with Intent.createChooser()
     * @author paulburke
     */
    public static Intent createGetContentIntent() {
        // Implicitly allow the user to select a particular kind of data
        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        // The MIME data type filter
        intent.setType("*/*");
        // Only return URIs that can be opened with ContentResolver
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        return intent;
    }
} public class RxUtils {

    public static void unsubscribeIfNotNull(Subscription subscription) {
        if (subscription != null) {
            subscription.unsubscribe();
        }
    }

    public static CompositeSubscription getNewCompositeSubIfUnsubscribed(CompositeSubscription subscription) {
        if (subscription == null || subscription.isUnsubscribed()) {
            return new CompositeSubscription();
        }

        return subscription;
    }

    //https://github.com/kaushikgopal/RxJava-Android-Samples/blob/master/app/src/main/java/com/morihacky/android/rxjava/ExponentialBackoffFragment.java
    @SuppressWarnings("ValueOfIncrementOrDecrementUsed")
    public static class RetryWithDelay
            implements Func1<Observable<? extends Throwable>, Observable<?>> {
        private final int _maxRetries;
        private final int _retryDelayMillis;
        private int _retryCount;


        public RetryWithDelay(final int maxRetries, final int retryDelayMillis) {
            _maxRetries = maxRetries;
            _retryDelayMillis = retryDelayMillis;
            _retryCount = 0;
        }


        @Override
        public Observable<?> call(Observable<? extends Throwable> attempts) {
            return attempts.flatMap(new Func1<Throwable, Observable<?>>() {
                @Override
                public Observable<?> call(Throwable throwable) {
                    if (++_retryCount < _maxRetries) {
                        // When this Observable calls onNext, the original
                        // Observable will be retried (i.e. re-subscribed).
                        Timber.d("Retry %d", _retryCount);
                        Timber.d("Retrying in %d ms", _retryCount * _retryDelayMillis);
                        return Observable.timer(_retryCount * _retryDelayMillis,
                                TimeUnit.MILLISECONDS);
                    }
                    // Max retries hit. Just pass the error along.
                    return Observable.error(throwable);
                }
            });
        }

        @Override
        public String toString() {
            return "RetryWithDelay{" +
                    "_maxRetries=" + _maxRetries +
                    ", _retryDelayMillis=" + _retryDelayMillis +
                    ", _retryCount=" + _retryCount +
                    '}';
        }
    }

    public interface SchedulerTransformer<T> extends Observable.Transformer<T, T> {
    }

    public static class AndroidSchedulerTransformer<T> implements SchedulerTransformer<T> {

        @Override public Observable<T> call(Observable<T> observable) {
            return observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
        }
    }

} public final static String FILE_BOOKMARK = "com.tevinjeffrey.vapor.services.FILE_BOOKMARK"; public final static String FILE_TEXT = "com.tevinjeffrey.vapor.services.FILE_TEXT"; public final static String FILE_TYPE = "com.tevinjeffrey.vapor.services.FILE_TYPE";
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.widget.Toast;
import com.squareup.otto.Bus;
import com.tevinjeffrey.vapor.R;
import com.tevinjeffrey.vapor.VaporApp;
import com.tevinjeffrey.vapor.events.UploadEvent;
import com.tevinjeffrey.vapor.events.UploadFailedEvent;
import com.tevinjeffrey.vapor.okcloudapp.CloudAppRequestBody;
import com.tevinjeffrey.vapor.okcloudapp.DataManager;
import com.tevinjeffrey.vapor.okcloudapp.ProgressListener;
import com.tevinjeffrey.vapor.okcloudapp.ProgressNotification;
import com.tevinjeffrey.vapor.okcloudapp.ProgressiveFileRequestBody;
import com.tevinjeffrey.vapor.okcloudapp.ProgressiveStringRequestBody;
import com.tevinjeffrey.vapor.okcloudapp.RefCountManager;
import com.tevinjeffrey.vapor.okcloudapp.exceptions.FileToLargeException;
import com.tevinjeffrey.vapor.okcloudapp.exceptions.UploadLimitException;
import com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem;
import com.tevinjeffrey.vapor.okcloudapp.utils.FileUtils;
import com.tevinjeffrey.vapor.utils.RxUtils;
import java.io.File;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import rx.Observable;
import rx.Observer;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
import timber.log.Timber;
import static com.tevinjeffrey.vapor.services.IntentBridge.FILE_BOOKMARK;
import static com.tevinjeffrey.vapor.services.IntentBridge.FILE_TEXT;
import static com.tevinjeffrey.vapor.services.IntentBridge.FILE_TYPE;
                        .setContentTitle(fileName)
                        .setContentText("Upload in Progress")
                        .setOngoing(true)
                        .addAction(0, "Cancel upload", PendingIntent.getService(UploadService.this,
                                notificationId,
                                new Intent(UploadService.this, UploadService.class)
                                        .putExtra(EXTRA_NOTIFICATION_ID, notificationId)
                                        .putExtra(ACTION_TYPE, ACTION_UPLOAD_CANCEL),
                                PendingIntent.FLAG_UPDATE_CURRENT));


                // Subject to reduce notification updates as the byes are written to the server.
                // The notification updates 4 times per second.
                final Subscription progressSubscription = progressSubject
                        .sample(250, TimeUnit.MILLISECONDS)
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnError(new Action1<Throwable>() {
                            @Override
                            public void call(Throwable throwable) {
                                Timber.e(throwable, "Error creating progress listener.");
                            }
                        })
                        .doOnUnsubscribe(new Action0() {
                            @Override
                            public void call() {
                                Timber.i("Progress unsubscribed %s", finalFileName);
                                mNotificationManager.cancel(notificationId);
                            }
                        })
                        .doOnNext(new ProgressNotification(
                                fileSize.length() == 0 ? 0 : Long.valueOf(fileSize),
                                uploadNotification,
                                mNotificationManager,
                                notificationId))
                        .onBackpressureDrop()
                        .subscribe();

                refCountManager.addNotificationId(notificationId);
                subscriptionHashMap.put(notificationId, uploadObservable.subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnUnsubscribe(new Action0() {
                            @Override
                            public void call() {
                                Timber.i("Upload unsubscribed %s", finalFileName);
                                RxUtils.unsubscribeIfNotNull(progressSubscription);
                            }
                        })
                        .doOnTerminate(new Action0() {
                            @Override
                            public void call() {
                                Timber.i("Upload terminated %s", finalFileName);
                                refCountManager.removeNotificationId(notificationId);
                            }
                        })
                        .doOnSubscribe(new Action0() {
                            @Override
                            public void call() {
                                Toast.makeText(getApplicationContext(), "Adding " + finalFileName + " to Vapor",
                                        Toast.LENGTH_SHORT).show();
                            }
                        })
                        .subscribe(new UploadObserver(notificationId, fileName, fileUri)));
                break;
        }

        return START_NOT_STICKY;
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        super.onTaskRemoved(rootIntent);
        for (Integer notificationId : refCountManager.getNotificationIds()) {
            Subscription subscription = subscriptionHashMap.get(notificationId);
            if (subscription != null) {
                subscription.unsubscribe();
            }
            mNotificationManager.cancel(notificationId);
            Timber.i("CLEAN UP from onTaskRemoved with id=%s", notificationId);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        for (Integer notificationId : refCountManager.getNotificationIds()) {
            Subscription subscription = subscriptionHashMap.get(notificationId);
            if (subscription != null) {
                subscription.unsubscribe();
            }
            mNotificationManager.cancel(notificationId);
            Timber.i("CLEAN UP from onDestroy with id=%s", notificationId);
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private class UploadObserver implements Observer<CloudAppItem> {
        int notificationId;
        final int newNotificationId;
        final String fileName;
        final Uri fileUri;

        public UploadObserver(int notificationId, String fileName, Uri fileUri) {
            this.newNotificationId = notificationId / 2;
            this.fileName = fileName;
            this.fileUri = fileUri;
        }

        @Override
        public void onCompleted() {
            Timber.i("Upload complete %s", fileName);
        }

        @Override
        public void onError(Throwable e) {
            Timber.e(e, "Error during upload. File: %s", fileName);
            if (e instanceof FileToLargeException || e instanceof UploadLimitException) {