300 lines
12 KiB
Java
300 lines
12 KiB
Java
/*
|
|
* Nextcloud Android client application
|
|
*
|
|
* @author Mario Danic
|
|
* @author Chris Narkiewicz
|
|
*
|
|
* Copyright (C) 2017 Mario Danic
|
|
* Copyright (C) 2017 Nextcloud
|
|
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
* License as published by the Free Software Foundation; either
|
|
* version 3 of the License, or any later version.
|
|
*
|
|
* This program 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 AFFERO GENERAL PUBLIC LICENSE for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public
|
|
* License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package com.owncloud.android.utils;
|
|
|
|
import android.accounts.Account;
|
|
import android.app.job.JobInfo;
|
|
import android.app.job.JobScheduler;
|
|
import android.content.ComponentName;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.provider.MediaStore;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import com.evernote.android.job.JobManager;
|
|
import com.evernote.android.job.JobRequest;
|
|
import com.evernote.android.job.util.Device;
|
|
import com.nextcloud.client.account.UserAccountManager;
|
|
import com.nextcloud.client.preferences.AppPreferences;
|
|
import com.owncloud.android.MainApp;
|
|
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
|
import com.owncloud.android.datamodel.FilesystemDataProvider;
|
|
import com.owncloud.android.datamodel.MediaFolderType;
|
|
import com.owncloud.android.datamodel.SyncedFolder;
|
|
import com.owncloud.android.datamodel.SyncedFolderProvider;
|
|
import com.owncloud.android.datamodel.UploadsStorageManager;
|
|
import com.owncloud.android.db.OCUpload;
|
|
import com.owncloud.android.files.services.FileUploader;
|
|
import com.owncloud.android.jobs.FilesSyncJob;
|
|
import com.owncloud.android.jobs.NContentObserverJob;
|
|
import com.owncloud.android.jobs.OfflineSyncJob;
|
|
|
|
import org.lukhnos.nnio.file.FileVisitResult;
|
|
import org.lukhnos.nnio.file.Files;
|
|
import org.lukhnos.nnio.file.Path;
|
|
import org.lukhnos.nnio.file.Paths;
|
|
import org.lukhnos.nnio.file.SimpleFileVisitor;
|
|
import org.lukhnos.nnio.file.attribute.BasicFileAttributes;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.Set;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import androidx.annotation.RequiresApi;
|
|
|
|
/**
|
|
* Various utilities that make auto upload tick
|
|
*/
|
|
public final class FilesSyncHelper {
|
|
public static final String TAG = "FileSyncHelper";
|
|
|
|
public static final String GLOBAL = "global";
|
|
public static final String SYNCEDFOLDERINITIATED = "syncedFolderIntitiated_";
|
|
|
|
public static final int ContentSyncJobId = 315;
|
|
|
|
private FilesSyncHelper() {
|
|
// utility class -> private constructor
|
|
}
|
|
|
|
public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder) {
|
|
final Context context = MainApp.getAppContext();
|
|
final ContentResolver contentResolver = context.getContentResolver();
|
|
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
|
|
|
|
Long currentTime = System.currentTimeMillis();
|
|
double currentTimeInSeconds = currentTime / 1000.0;
|
|
String currentTimeString = Long.toString((long) currentTimeInSeconds);
|
|
|
|
String syncedFolderInitiatedKey = SYNCEDFOLDERINITIATED + syncedFolder.getId();
|
|
boolean dryRun = TextUtils.isEmpty(arbitraryDataProvider.getValue
|
|
(GLOBAL, syncedFolderInitiatedKey));
|
|
|
|
if (MediaFolderType.IMAGE == syncedFolder.getType()) {
|
|
if (dryRun) {
|
|
arbitraryDataProvider.storeOrUpdateKeyValue(GLOBAL, syncedFolderInitiatedKey,
|
|
currentTimeString);
|
|
} else {
|
|
FilesSyncHelper.insertContentIntoDB(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI
|
|
, syncedFolder);
|
|
FilesSyncHelper.insertContentIntoDB(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
|
syncedFolder);
|
|
}
|
|
|
|
} else if (MediaFolderType.VIDEO == syncedFolder.getType()) {
|
|
|
|
if (dryRun) {
|
|
arbitraryDataProvider.storeOrUpdateKeyValue(GLOBAL, syncedFolderInitiatedKey,
|
|
currentTimeString);
|
|
} else {
|
|
FilesSyncHelper.insertContentIntoDB(android.provider.MediaStore.Video.Media.INTERNAL_CONTENT_URI,
|
|
syncedFolder);
|
|
FilesSyncHelper.insertContentIntoDB(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
|
|
syncedFolder);
|
|
}
|
|
|
|
} else {
|
|
try {
|
|
if (dryRun) {
|
|
arbitraryDataProvider.storeOrUpdateKeyValue(GLOBAL, syncedFolderInitiatedKey,
|
|
currentTimeString);
|
|
} else {
|
|
FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
|
|
Path path = Paths.get(syncedFolder.getLocalPath());
|
|
|
|
String dateInitiated = arbitraryDataProvider.getValue(GLOBAL,
|
|
syncedFolderInitiatedKey);
|
|
|
|
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
|
@Override
|
|
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
|
|
|
|
File file = path.toFile();
|
|
if (attrs.lastModifiedTime().toMillis() >= Long.parseLong(dateInitiated) * 1000) {
|
|
filesystemDataProvider.storeOrUpdateFileValue(path.toAbsolutePath().toString(),
|
|
attrs.lastModifiedTime().toMillis(), file.isDirectory(), syncedFolder);
|
|
}
|
|
|
|
return FileVisitResult.CONTINUE;
|
|
}
|
|
|
|
@Override
|
|
public FileVisitResult visitFileFailed(Path file, IOException exc) {
|
|
return FileVisitResult.CONTINUE;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
Log.e(TAG, "Something went wrong while indexing files for auto upload " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void insertAllDBEntries(AppPreferences preferences, boolean skipCustom) {
|
|
final Context context = MainApp.getAppContext();
|
|
final ContentResolver contentResolver = context.getContentResolver();
|
|
SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver,
|
|
preferences);
|
|
|
|
for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
|
|
if (syncedFolder.isEnabled() && (MediaFolderType.CUSTOM != syncedFolder.getType() || !skipCustom)) {
|
|
insertAllDBEntriesForSyncedFolder(syncedFolder);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void insertContentIntoDB(Uri uri, SyncedFolder syncedFolder) {
|
|
final Context context = MainApp.getAppContext();
|
|
final ContentResolver contentResolver = context.getContentResolver();
|
|
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
|
|
|
|
Cursor cursor;
|
|
int column_index_data;
|
|
int column_index_date_modified;
|
|
|
|
final FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
|
|
|
|
String contentPath;
|
|
boolean isFolder;
|
|
|
|
String[] projection = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DATE_MODIFIED};
|
|
|
|
String path = syncedFolder.getLocalPath();
|
|
if (!path.endsWith("/")) {
|
|
path = path + "/%";
|
|
} else {
|
|
path = path + "%";
|
|
}
|
|
|
|
String syncedFolderInitiatedKey = SYNCEDFOLDERINITIATED + syncedFolder.getId();
|
|
String dateInitiated = arbitraryDataProvider.getValue(GLOBAL, syncedFolderInitiatedKey);
|
|
|
|
cursor = context.getContentResolver().query(uri, projection, MediaStore.MediaColumns.DATA + " LIKE ?",
|
|
new String[]{path}, null);
|
|
|
|
if (cursor != null) {
|
|
column_index_data = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
|
|
column_index_date_modified = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED);
|
|
while (cursor.moveToNext()) {
|
|
contentPath = cursor.getString(column_index_data);
|
|
isFolder = new File(contentPath).isDirectory();
|
|
if (cursor.getLong(column_index_date_modified) >= Long.parseLong(dateInitiated)) {
|
|
filesystemDataProvider.storeOrUpdateFileValue(contentPath,
|
|
cursor.getLong(column_index_date_modified), isFolder, syncedFolder);
|
|
}
|
|
}
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
public static void restartJobsIfNeeded(UploadsStorageManager uploadsStorageManager, UserAccountManager accountManager) {
|
|
final Context context = MainApp.getAppContext();
|
|
|
|
FileUploader.UploadRequester uploadRequester = new FileUploader.UploadRequester();
|
|
|
|
boolean accountExists;
|
|
|
|
OCUpload[] failedUploads = uploadsStorageManager.getFailedUploads();
|
|
|
|
for (OCUpload failedUpload : failedUploads) {
|
|
accountExists = false;
|
|
|
|
// check if accounts still exists
|
|
for (Account account : accountManager.getAccounts()) {
|
|
if (account.name.equals(failedUpload.getAccountName())) {
|
|
accountExists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!accountExists) {
|
|
uploadsStorageManager.removeUpload(failedUpload);
|
|
}
|
|
}
|
|
|
|
new Thread(() -> {
|
|
if (!Device.getNetworkType(context).equals(JobRequest.NetworkType.ANY) &&
|
|
!ConnectivityUtils.isInternetWalled(context)) {
|
|
uploadRequester.retryFailedUploads(context, null, uploadsStorageManager, null);
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
public static void scheduleFilesSyncIfNeeded(Context context) {
|
|
// always run this because it also allows us to perform retries of manual uploads
|
|
new JobRequest.Builder(FilesSyncJob.TAG)
|
|
.setPeriodic(900000L, 300000L)
|
|
.setUpdateCurrent(true)
|
|
.build()
|
|
.schedule();
|
|
|
|
if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
scheduleJobOnN();
|
|
}
|
|
}
|
|
|
|
public static void scheduleOfflineSyncIfNeeded() {
|
|
Set<JobRequest> jobRequests = JobManager.instance().getAllJobRequestsForTag(OfflineSyncJob.TAG);
|
|
if (jobRequests.isEmpty()) {
|
|
new JobRequest.Builder(OfflineSyncJob.TAG)
|
|
.setPeriodic(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(5))
|
|
.setUpdateCurrent(false)
|
|
.build()
|
|
.schedule();
|
|
}
|
|
}
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
public static void scheduleJobOnN() {
|
|
JobScheduler jobScheduler = MainApp.getAppContext().getSystemService(JobScheduler.class);
|
|
|
|
if (jobScheduler != null) {
|
|
JobInfo.Builder builder = new JobInfo.Builder(ContentSyncJobId, new ComponentName(MainApp.getAppContext(),
|
|
NContentObserverJob.class.getName()));
|
|
builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore.
|
|
Images.Media.INTERNAL_CONTENT_URI,
|
|
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
|
|
builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore.
|
|
Images.Media.EXTERNAL_CONTENT_URI,
|
|
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
|
|
builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore.
|
|
Video.Media.INTERNAL_CONTENT_URI,
|
|
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
|
|
builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore.
|
|
Video.Media.EXTERNAL_CONTENT_URI,
|
|
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
|
|
builder.setTriggerContentMaxDelay(1500);
|
|
jobScheduler.schedule(builder.build());
|
|
}
|
|
}
|
|
}
|
|
|