repo created
This commit is contained in:
commit
1ef725ef20
2483 changed files with 278273 additions and 0 deletions
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.files
|
||||
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.resources.files.model.FileLockType
|
||||
|
||||
object FileLockingHelper {
|
||||
/**
|
||||
* Checks whether the given `userId` can unlock the [OCFile].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun canUserUnlockFile(userId: String, file: OCFile): Boolean {
|
||||
if (!file.isLocked || file.lockOwnerId == null || file.lockType != FileLockType.MANUAL) {
|
||||
return false
|
||||
}
|
||||
return file.lockOwnerId == userId
|
||||
}
|
||||
}
|
||||
33
app/src/main/java/com/nextcloud/android/sso/Constants.java
Normal file
33
app/src/main/java/com/nextcloud/android/sso/Constants.java
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 David Luhmer <david-dev@live.de>
|
||||
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2019 Edvard Holst <edvard.holst@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.sso;
|
||||
|
||||
public final class Constants {
|
||||
// Authenticator related constants
|
||||
public static final String SSO_USER_ID = "user_id";
|
||||
public static final String SSO_TOKEN = "token";
|
||||
public static final String SSO_SERVER_URL = "server_url";
|
||||
public static final String SSO_SHARED_PREFERENCE = "single-sign-on";
|
||||
public static final String NEXTCLOUD_SSO_EXCEPTION = "NextcloudSsoException";
|
||||
public static final String NEXTCLOUD_SSO = "NextcloudSSO";
|
||||
public static final String NEXTCLOUD_FILES_ACCOUNT = "NextcloudFilesAccount";
|
||||
public static final String DELIMITER = "_";
|
||||
|
||||
// Custom Exceptions
|
||||
public static final String EXCEPTION_INVALID_TOKEN = "CE_1";
|
||||
public static final String EXCEPTION_ACCOUNT_NOT_FOUND = "CE_2";
|
||||
public static final String EXCEPTION_UNSUPPORTED_METHOD = "CE_3";
|
||||
public static final String EXCEPTION_INVALID_REQUEST_URL = "CE_4";
|
||||
public static final String EXCEPTION_HTTP_REQUEST_FAILED = "CE_5";
|
||||
public static final String EXCEPTION_ACCOUNT_ACCESS_DECLINED = "CE_6";
|
||||
|
||||
private Constants() {
|
||||
// No instance
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,524 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 David Luhmer <david-dev@live.de>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* More information here: https://github.com/abeluck/android-streams-ipc
|
||||
*/
|
||||
package com.nextcloud.android.sso;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.nextcloud.android.sso.aidl.IInputStreamService;
|
||||
import com.nextcloud.android.sso.aidl.NextcloudRequest;
|
||||
import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount;
|
||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManager;
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.utils.EncryptionUtils;
|
||||
|
||||
import org.apache.commons.httpclient.HttpConnection;
|
||||
import org.apache.commons.httpclient.HttpMethodBase;
|
||||
import org.apache.commons.httpclient.HttpState;
|
||||
import org.apache.commons.httpclient.NameValuePair;
|
||||
import org.apache.commons.httpclient.methods.DeleteMethod;
|
||||
import org.apache.commons.httpclient.methods.GetMethod;
|
||||
import org.apache.commons.httpclient.methods.HeadMethod;
|
||||
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
|
||||
import org.apache.commons.httpclient.methods.PostMethod;
|
||||
import org.apache.commons.httpclient.methods.PutMethod;
|
||||
import org.apache.commons.httpclient.methods.RequestEntity;
|
||||
import org.apache.commons.httpclient.methods.StringRequestEntity;
|
||||
import org.apache.jackrabbit.webdav.DavConstants;
|
||||
import org.apache.jackrabbit.webdav.client.methods.MkColMethod;
|
||||
import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import static com.nextcloud.android.sso.Constants.DELIMITER;
|
||||
import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_NOT_FOUND;
|
||||
import static com.nextcloud.android.sso.Constants.EXCEPTION_HTTP_REQUEST_FAILED;
|
||||
import static com.nextcloud.android.sso.Constants.EXCEPTION_INVALID_REQUEST_URL;
|
||||
import static com.nextcloud.android.sso.Constants.EXCEPTION_INVALID_TOKEN;
|
||||
import static com.nextcloud.android.sso.Constants.EXCEPTION_UNSUPPORTED_METHOD;
|
||||
import static com.nextcloud.android.sso.Constants.SSO_SHARED_PREFERENCE;
|
||||
|
||||
|
||||
/**
|
||||
* Stream binder to pass usable InputStreams across the process boundary in Android.
|
||||
*/
|
||||
public class InputStreamBinder extends IInputStreamService.Stub {
|
||||
|
||||
private final static String TAG = "InputStreamBinder";
|
||||
private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
|
||||
private static final String CHARSET_UTF8 = "UTF-8";
|
||||
|
||||
private static final int HTTP_STATUS_CODE_OK = 200;
|
||||
private static final int HTTP_STATUS_CODE_MULTIPLE_CHOICES = 300;
|
||||
|
||||
private static final char PATH_SEPARATOR = '/';
|
||||
private static final int ZERO_LENGTH = 0;
|
||||
private Context context;
|
||||
private UserAccountManager accountManager;
|
||||
|
||||
public InputStreamBinder(Context context, UserAccountManager accountManager) {
|
||||
this.context = context;
|
||||
this.accountManager = accountManager;
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor performNextcloudRequestV2(ParcelFileDescriptor input) {
|
||||
return performNextcloudRequestAndBodyStreamV2(input, null);
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor performNextcloudRequestAndBodyStreamV2(
|
||||
ParcelFileDescriptor input,
|
||||
ParcelFileDescriptor requestBodyParcelFileDescriptor) {
|
||||
// read the input
|
||||
final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input);
|
||||
|
||||
final InputStream requestBodyInputStream = requestBodyParcelFileDescriptor != null ?
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(requestBodyParcelFileDescriptor) : null;
|
||||
Exception exception = null;
|
||||
Response response = new Response();
|
||||
|
||||
try {
|
||||
// Start request and catch exceptions
|
||||
NextcloudRequest request = deserializeObjectAndCloseStream(is);
|
||||
response = processRequestV2(request, requestBodyInputStream);
|
||||
} catch (Exception e) {
|
||||
Log_OC.e(TAG, "Error during Nextcloud request", e);
|
||||
exception = e;
|
||||
}
|
||||
|
||||
try {
|
||||
// Write exception to the stream followed by the actual network stream
|
||||
InputStream exceptionStream = serializeObjectToInputStreamV2(exception, response.getPlainHeadersString());
|
||||
InputStream resultStream = new java.io.SequenceInputStream(exceptionStream, response.getBody());
|
||||
|
||||
return ParcelFileDescriptorUtil.pipeFrom(resultStream,
|
||||
thread -> Log_OC.d(TAG, "Done sending result"),
|
||||
response.getMethod());
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(TAG, "Error while sending response back to client app", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor performNextcloudRequest(ParcelFileDescriptor input) {
|
||||
return performNextcloudRequestAndBodyStream(input, null);
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor performNextcloudRequestAndBodyStream(
|
||||
ParcelFileDescriptor input,
|
||||
ParcelFileDescriptor requestBodyParcelFileDescriptor) {
|
||||
// read the input
|
||||
final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input);
|
||||
|
||||
final InputStream requestBodyInputStream = requestBodyParcelFileDescriptor != null ?
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(requestBodyParcelFileDescriptor) : null;
|
||||
Exception exception = null;
|
||||
HttpMethodBase httpMethod = null;
|
||||
InputStream httpStream = new InputStream() {
|
||||
@Override
|
||||
public int read() {
|
||||
return ZERO_LENGTH;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Start request and catch exceptions
|
||||
NextcloudRequest request = deserializeObjectAndCloseStream(is);
|
||||
httpMethod = processRequest(request, requestBodyInputStream);
|
||||
httpStream = httpMethod.getResponseBodyAsStream();
|
||||
} catch (Exception e) {
|
||||
Log_OC.e(TAG, "Error during Nextcloud request", e);
|
||||
exception = e;
|
||||
}
|
||||
|
||||
try {
|
||||
// Write exception to the stream followed by the actual network stream
|
||||
InputStream exceptionStream = serializeObjectToInputStream(exception);
|
||||
InputStream resultStream;
|
||||
if (httpStream != null) {
|
||||
resultStream = new java.io.SequenceInputStream(exceptionStream, httpStream);
|
||||
} else {
|
||||
resultStream = exceptionStream;
|
||||
}
|
||||
return ParcelFileDescriptorUtil.pipeFrom(resultStream,
|
||||
thread -> Log_OC.d(TAG, "Done sending result"),
|
||||
httpMethod);
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(TAG, "Error while sending response back to client app", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ByteArrayInputStream serializeObjectToInputStreamV2(Exception exception, String headers) {
|
||||
byte[] baosByteArray = new byte[0];
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
|
||||
oos.writeObject(exception);
|
||||
oos.writeObject(headers);
|
||||
oos.flush();
|
||||
oos.close();
|
||||
|
||||
baosByteArray = baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(TAG, "Error while sending response back to client app", e);
|
||||
}
|
||||
|
||||
return new ByteArrayInputStream(baosByteArray);
|
||||
}
|
||||
|
||||
private <T extends Serializable> ByteArrayInputStream serializeObjectToInputStream(T obj) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||
oos.writeObject(obj);
|
||||
oos.flush();
|
||||
oos.close();
|
||||
return new ByteArrayInputStream(baos.toByteArray());
|
||||
}
|
||||
|
||||
private <T extends Serializable> T deserializeObjectAndCloseStream(InputStream is) throws IOException,
|
||||
ClassNotFoundException {
|
||||
ObjectInputStream ois = new ObjectInputStream(is);
|
||||
T result = (T) ois.readObject();
|
||||
is.close();
|
||||
ois.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
public class NCPropFindMethod extends PropFindMethod {
|
||||
NCPropFindMethod(String uri, int propfindType, int depth) throws IOException {
|
||||
super(uri, propfindType, new DavPropertyNameSet(), depth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
|
||||
// Do not process the response body here. Instead pass it on to client app.
|
||||
}
|
||||
}
|
||||
|
||||
private HttpMethodBase buildMethod(NextcloudRequest request, Uri baseUri, InputStream requestBodyInputStream)
|
||||
throws IOException {
|
||||
String requestUrl = baseUri + request.getUrl();
|
||||
HttpMethodBase method;
|
||||
switch (request.getMethod()) {
|
||||
case "GET":
|
||||
method = new GetMethod(requestUrl);
|
||||
break;
|
||||
|
||||
case "POST":
|
||||
method = new PostMethod(requestUrl);
|
||||
if (requestBodyInputStream != null) {
|
||||
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1);
|
||||
((PostMethod) method).setRequestEntity(requestEntity);
|
||||
} else if (request.getRequestBody() != null) {
|
||||
StringRequestEntity requestEntity = new StringRequestEntity(
|
||||
request.getRequestBody(),
|
||||
CONTENT_TYPE_APPLICATION_JSON,
|
||||
CHARSET_UTF8);
|
||||
((PostMethod) method).setRequestEntity(requestEntity);
|
||||
}
|
||||
break;
|
||||
|
||||
case "PATCH":
|
||||
method = new PatchMethod(requestUrl);
|
||||
if (requestBodyInputStream != null) {
|
||||
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1);
|
||||
((PatchMethod) method).setRequestEntity(requestEntity);
|
||||
} else if (request.getRequestBody() != null) {
|
||||
StringRequestEntity requestEntity = new StringRequestEntity(
|
||||
request.getRequestBody(),
|
||||
CONTENT_TYPE_APPLICATION_JSON,
|
||||
CHARSET_UTF8);
|
||||
((PatchMethod) method).setRequestEntity(requestEntity);
|
||||
}
|
||||
break;
|
||||
|
||||
case "PUT":
|
||||
method = new PutMethod(requestUrl);
|
||||
if (requestBodyInputStream != null) {
|
||||
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1);
|
||||
((PutMethod) method).setRequestEntity(requestEntity);
|
||||
} else if (request.getRequestBody() != null) {
|
||||
StringRequestEntity requestEntity = new StringRequestEntity(
|
||||
request.getRequestBody(),
|
||||
CONTENT_TYPE_APPLICATION_JSON,
|
||||
CHARSET_UTF8);
|
||||
((PutMethod) method).setRequestEntity(requestEntity);
|
||||
}
|
||||
break;
|
||||
|
||||
case "DELETE":
|
||||
method = new DeleteMethod(requestUrl);
|
||||
break;
|
||||
|
||||
case "PROPFIND":
|
||||
method = new NCPropFindMethod(requestUrl, DavConstants.PROPFIND_ALL_PROP, DavConstants.DEPTH_1);
|
||||
if (request.getRequestBody() != null) {
|
||||
//text/xml; charset=UTF-8 is taken from XmlRequestEntity... Should be application/xml
|
||||
StringRequestEntity requestEntity = new StringRequestEntity(
|
||||
request.getRequestBody(),
|
||||
"text/xml; charset=UTF-8",
|
||||
CHARSET_UTF8);
|
||||
((PropFindMethod) method).setRequestEntity(requestEntity);
|
||||
}
|
||||
break;
|
||||
|
||||
case "MKCOL":
|
||||
method = new MkColMethod(requestUrl);
|
||||
break;
|
||||
|
||||
case "HEAD":
|
||||
method = new HeadMethod(requestUrl);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException(EXCEPTION_UNSUPPORTED_METHOD);
|
||||
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private HttpMethodBase processRequest(final NextcloudRequest request, final InputStream requestBodyInputStream)
|
||||
throws UnsupportedOperationException,
|
||||
com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException,
|
||||
OperationCanceledException, AuthenticatorException, IOException {
|
||||
Account account = accountManager.getAccountByName(request.getAccountName());
|
||||
if (account == null) {
|
||||
throw new IllegalStateException(EXCEPTION_ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Validate token
|
||||
if (!isValid(request)) {
|
||||
throw new IllegalStateException(EXCEPTION_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
if (request.getUrl().length() == 0 || request.getUrl().charAt(0) != PATH_SEPARATOR) {
|
||||
throw new IllegalStateException(EXCEPTION_INVALID_REQUEST_URL,
|
||||
new IllegalStateException("URL need to start with a /"));
|
||||
}
|
||||
|
||||
OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton();
|
||||
OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
|
||||
OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context);
|
||||
|
||||
HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream);
|
||||
|
||||
if (request.getParameterV2() != null && !request.getParameterV2().isEmpty()) {
|
||||
method.setQueryString(convertListToNVP(request.getParameterV2()));
|
||||
} else {
|
||||
method.setQueryString(convertMapToNVP(request.getParameter()));
|
||||
}
|
||||
method.addRequestHeader("OCS-APIREQUEST", "true");
|
||||
|
||||
for (Map.Entry<String, List<String>> header : request.getHeader().entrySet()) {
|
||||
// https://stackoverflow.com/a/3097052
|
||||
method.addRequestHeader(header.getKey(), TextUtils.join(",", header.getValue()));
|
||||
|
||||
if ("OCS-APIREQUEST".equalsIgnoreCase(header.getKey())) {
|
||||
throw new IllegalStateException(
|
||||
"The 'OCS-APIREQUEST' header will be automatically added by the Nextcloud SSO Library. " +
|
||||
"Please remove the header before making a request");
|
||||
}
|
||||
}
|
||||
|
||||
client.setFollowRedirects(request.isFollowRedirects());
|
||||
int status = client.executeMethod(method);
|
||||
|
||||
// Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
|
||||
if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) {
|
||||
return method;
|
||||
} else {
|
||||
InputStream inputStream = method.getResponseBodyAsStream();
|
||||
String total = "No response body";
|
||||
|
||||
// If response body is available
|
||||
if (inputStream != null) {
|
||||
total = inputStreamToString(inputStream);
|
||||
Log_OC.e(TAG, total);
|
||||
}
|
||||
|
||||
method.releaseConnection();
|
||||
throw new IllegalStateException(EXCEPTION_HTTP_REQUEST_FAILED,
|
||||
new IllegalStateException(String.valueOf(status),
|
||||
new IllegalStateException(total)));
|
||||
}
|
||||
}
|
||||
|
||||
private Response processRequestV2(final NextcloudRequest request, final InputStream requestBodyInputStream)
|
||||
throws UnsupportedOperationException,
|
||||
com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException,
|
||||
OperationCanceledException, AuthenticatorException, IOException {
|
||||
Account account = accountManager.getAccountByName(request.getAccountName());
|
||||
if (account == null) {
|
||||
throw new IllegalStateException(EXCEPTION_ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Validate token
|
||||
if (!isValid(request)) {
|
||||
throw new IllegalStateException(EXCEPTION_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
if (request.getUrl().length() == 0 || request.getUrl().charAt(0) != PATH_SEPARATOR) {
|
||||
throw new IllegalStateException(EXCEPTION_INVALID_REQUEST_URL,
|
||||
new IllegalStateException("URL need to start with a /"));
|
||||
}
|
||||
|
||||
OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton();
|
||||
OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
|
||||
OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context);
|
||||
|
||||
HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream);
|
||||
|
||||
if (request.getParameterV2() != null && !request.getParameterV2().isEmpty()) {
|
||||
method.setQueryString(convertListToNVP(request.getParameterV2()));
|
||||
} else {
|
||||
method.setQueryString(convertMapToNVP(request.getParameter()));
|
||||
}
|
||||
|
||||
method.addRequestHeader("OCS-APIREQUEST", "true");
|
||||
|
||||
for (Map.Entry<String, List<String>> header : request.getHeader().entrySet()) {
|
||||
// https://stackoverflow.com/a/3097052
|
||||
method.addRequestHeader(header.getKey(), TextUtils.join(",", header.getValue()));
|
||||
|
||||
if ("OCS-APIREQUEST".equalsIgnoreCase(header.getKey())) {
|
||||
throw new IllegalStateException(
|
||||
"The 'OCS-APIREQUEST' header will be automatically added by the Nextcloud SSO Library. " +
|
||||
"Please remove the header before making a request");
|
||||
}
|
||||
}
|
||||
|
||||
client.setFollowRedirects(request.isFollowRedirects());
|
||||
int status = client.executeMethod(method);
|
||||
|
||||
// Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
|
||||
if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) {
|
||||
return new Response(method);
|
||||
} else {
|
||||
InputStream inputStream = method.getResponseBodyAsStream();
|
||||
String total = "No response body";
|
||||
|
||||
// If response body is available
|
||||
if (inputStream != null) {
|
||||
total = inputStreamToString(inputStream);
|
||||
Log_OC.e(TAG, total);
|
||||
}
|
||||
|
||||
method.releaseConnection();
|
||||
throw new IllegalStateException(EXCEPTION_HTTP_REQUEST_FAILED,
|
||||
new IllegalStateException(String.valueOf(status),
|
||||
new IllegalStateException(total)));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValid(NextcloudRequest request) {
|
||||
String[] callingPackageNames = context.getPackageManager().getPackagesForUid(Binder.getCallingUid());
|
||||
|
||||
SharedPreferences sharedPreferences = context.getSharedPreferences(SSO_SHARED_PREFERENCE,
|
||||
Context.MODE_PRIVATE);
|
||||
for (String callingPackageName : callingPackageNames) {
|
||||
String hash = sharedPreferences.getString(callingPackageName + DELIMITER + request.getAccountName(), "");
|
||||
if (hash.isEmpty())
|
||||
continue;
|
||||
if (validateToken(hash, request.getToken())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean validateToken(String hash, String token) {
|
||||
if (!hash.contains("$")) {
|
||||
throw new IllegalStateException(EXCEPTION_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
String salt = hash.split("\\$")[1]; // TODO extract "$"
|
||||
|
||||
String newHash = EncryptionUtils.generateSHA512(token, salt);
|
||||
|
||||
// As discussed with Lukas R. at the Nextcloud Conf 2018, always compare whole strings
|
||||
// and don't exit prematurely if the string does not match anymore to prevent timing-attacks
|
||||
return isEqual(hash.getBytes(), newHash.getBytes());
|
||||
}
|
||||
|
||||
// Taken from http://codahale.com/a-lesson-in-timing-attacks/
|
||||
private static boolean isEqual(byte[] a, byte[] b) {
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
result |= a[i] ^ b[i];
|
||||
}
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
private static String inputStreamToString(InputStream inputStream) {
|
||||
try {
|
||||
StringBuilder total = new StringBuilder();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
String line = reader.readLine();
|
||||
while (line != null) {
|
||||
total.append(line).append('\n');
|
||||
line = reader.readLine();
|
||||
}
|
||||
return total.toString();
|
||||
} catch (Exception e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static NameValuePair[] convertMapToNVP(Map<String, String> map) {
|
||||
NameValuePair[] nvp = new NameValuePair[map.size()];
|
||||
int i = 0;
|
||||
for (String key : map.keySet()) {
|
||||
nvp[i] = new NameValuePair(key, map.get(key));
|
||||
i++;
|
||||
}
|
||||
return nvp;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static NameValuePair[] convertListToNVP(Collection<QueryParam> list) {
|
||||
NameValuePair[] nvp = new NameValuePair[list.size()];
|
||||
int i = 0;
|
||||
for (QueryParam pair : list) {
|
||||
nvp[i] = new NameValuePair(pair.key, pair.value);
|
||||
i++;
|
||||
}
|
||||
return nvp;
|
||||
}
|
||||
}
|
||||
100
app/src/main/java/com/nextcloud/android/sso/PatchMethod.java
Normal file
100
app/src/main/java/com/nextcloud/android/sso/PatchMethod.java
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Timo Triebensky <timo@binsky.org>
|
||||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* More information here: https://github.com/abeluck/android-streams-ipc
|
||||
*/
|
||||
package com.nextcloud.android.sso;
|
||||
|
||||
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
|
||||
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
|
||||
import org.apache.commons.httpclient.methods.PostMethod;
|
||||
import org.apache.commons.httpclient.methods.RequestEntity;
|
||||
import org.apache.commons.httpclient.util.EncodingUtil;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
public class PatchMethod extends PostMethod {
|
||||
|
||||
/**
|
||||
* The buffered request body consisting of <code>NameValuePair</code>s.
|
||||
*/
|
||||
private Vector params = new Vector();
|
||||
|
||||
/**
|
||||
* No-arg constructor.
|
||||
*/
|
||||
public PatchMethod() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor specifying a URI.
|
||||
*
|
||||
* @param uri either an absolute or relative URI
|
||||
*/
|
||||
public PatchMethod(String uri) {
|
||||
super(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>"PATCH"</tt>.
|
||||
*
|
||||
* @return <tt>"PATCH"</tt>
|
||||
* @since 2.0
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PATCH";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if there is a request body to be sent.
|
||||
*
|
||||
* @return boolean
|
||||
* @since 2.0beta1
|
||||
*/
|
||||
protected boolean hasRequestContent() {
|
||||
if (!this.params.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
return super.hasRequestContent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears request body.
|
||||
*
|
||||
* @since 2.0beta1
|
||||
*/
|
||||
protected void clearRequestBody() {
|
||||
this.params.clear();
|
||||
super.clearRequestBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a request entity from the patch parameters, if present. Calls {@link
|
||||
* EntityEnclosingMethod#generateRequestBody()} if parameters have not been set.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
protected RequestEntity generateRequestEntity() {
|
||||
if (!this.params.isEmpty()) {
|
||||
// Use a ByteArrayRequestEntity instead of a StringRequestEntity.
|
||||
// This is to avoid potential encoding issues. Form url encoded strings
|
||||
// are ASCII by definition but the content type may not be. Treating the content
|
||||
// as bytes allows us to keep the current charset without worrying about how
|
||||
// this charset will effect the encoding of the form url encoded string.
|
||||
String content = EncodingUtil.formUrlEncode(getParameters(), getRequestCharSet());
|
||||
return new ByteArrayRequestEntity(
|
||||
EncodingUtil.getAsciiBytes(content),
|
||||
FORM_URL_ENCODED_CONTENT_TYPE
|
||||
);
|
||||
} else {
|
||||
return super.generateRequestEntity();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
app/src/main/java/com/nextcloud/android/sso/PlainHeader.java
Normal file
43
app/src/main/java/com/nextcloud/android/sso/PlainHeader.java
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.sso;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class PlainHeader implements Serializable {
|
||||
private static final long serialVersionUID = 3284979177401282512L;
|
||||
|
||||
private String name;
|
||||
private String value;
|
||||
|
||||
PlainHeader(String name, String value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private void writeObject(ObjectOutputStream oos) throws IOException {
|
||||
oos.writeObject(name);
|
||||
oos.writeObject(value);
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
name = (String) in.readObject();
|
||||
value = (String) in.readObject();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
22
app/src/main/java/com/nextcloud/android/sso/QueryParam.java
Normal file
22
app/src/main/java/com/nextcloud/android/sso/QueryParam.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.sso;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class QueryParam implements Serializable {
|
||||
private static final long serialVersionUID = 21523240203234211L; // must be same as in SSO project
|
||||
|
||||
public String key;
|
||||
public String value;
|
||||
|
||||
public QueryParam(String key, String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
59
app/src/main/java/com/nextcloud/android/sso/Response.java
Normal file
59
app/src/main/java/com/nextcloud/android/sso/Response.java
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.sso;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.apache.commons.httpclient.Header;
|
||||
import org.apache.commons.httpclient.HttpMethodBase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Response {
|
||||
private InputStream body;
|
||||
private Header[] headers;
|
||||
private HttpMethodBase method;
|
||||
|
||||
public Response() {
|
||||
headers = new Header[0];
|
||||
body = new InputStream() {
|
||||
@Override
|
||||
public int read() {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Response(HttpMethodBase methodBase) throws IOException {
|
||||
this.method = methodBase;
|
||||
this.body = methodBase.getResponseBodyAsStream();
|
||||
this.headers = methodBase.getResponseHeaders();
|
||||
}
|
||||
|
||||
public String getPlainHeadersString() {
|
||||
List<PlainHeader> arrayList = new ArrayList<>(headers.length);
|
||||
|
||||
for (Header header : headers) {
|
||||
arrayList.add(new PlainHeader(header.getName(), header.getValue()));
|
||||
}
|
||||
|
||||
Gson gson = new Gson();
|
||||
return gson.toJson(arrayList);
|
||||
}
|
||||
|
||||
public InputStream getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
public HttpMethodBase getMethod() {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017 David Luhmer <david-dev@live.de>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.sso.aidl;
|
||||
|
||||
public interface IThreadListener {
|
||||
|
||||
void onThreadFinished(final Thread thread);
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
|
||||
* SPDX-FileCopyrightText: 2017 David Luhmer <david-dev@live.de>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.sso.aidl;
|
||||
|
||||
import com.nextcloud.android.sso.QueryParam;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class NextcloudRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 215521212534240L; //assign a long value
|
||||
|
||||
private String method;
|
||||
private Map<String, List<String>> header = new HashMap<>();
|
||||
private Map<String, String> parameter = new HashMap<>();
|
||||
private final Collection<QueryParam> parameterV2 = new LinkedList<>();
|
||||
private String requestBody;
|
||||
private String url;
|
||||
private String token;
|
||||
private String packageName;
|
||||
private String accountName;
|
||||
private boolean followRedirects;
|
||||
|
||||
private NextcloudRequest() { }
|
||||
|
||||
public static class Builder {
|
||||
private NextcloudRequest ncr;
|
||||
|
||||
public Builder() {
|
||||
ncr = new NextcloudRequest();
|
||||
}
|
||||
|
||||
public NextcloudRequest build() {
|
||||
return ncr;
|
||||
}
|
||||
|
||||
public Builder setMethod(String method) {
|
||||
ncr.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHeader(Map<String, List<String>> header) {
|
||||
ncr.header = header;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setParameter(Map<String, String> parameter) {
|
||||
ncr.parameter = parameter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRequestBody(String requestBody) {
|
||||
ncr.requestBody = requestBody;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUrl(String url) {
|
||||
ncr.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setToken(String token) {
|
||||
ncr.token = token;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAccountName(String accountName) {
|
||||
ncr.accountName = accountName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default value: true
|
||||
* @param followRedirects
|
||||
* @return
|
||||
*/
|
||||
public Builder setFollowRedirects(boolean followRedirects) {
|
||||
ncr.followRedirects = followRedirects;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getHeader() {
|
||||
return this.header;
|
||||
}
|
||||
|
||||
public Map<String, String> getParameter() {
|
||||
return this.parameter;
|
||||
}
|
||||
|
||||
public String getRequestBody() {
|
||||
return this.requestBody;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return this.packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return this.accountName;
|
||||
}
|
||||
|
||||
public void setAccountName(String accountName) {
|
||||
this.accountName = accountName;
|
||||
}
|
||||
|
||||
public boolean isFollowRedirects() {
|
||||
return this.followRedirects;
|
||||
}
|
||||
|
||||
public Collection<QueryParam> getParameterV2() {
|
||||
return parameterV2;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017 David Luhmer <david-dev@live.de>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.android.sso.aidl;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
|
||||
import org.apache.commons.httpclient.HttpMethodBase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public final class ParcelFileDescriptorUtil {
|
||||
|
||||
private ParcelFileDescriptorUtil() { }
|
||||
|
||||
public static ParcelFileDescriptor pipeFrom(InputStream inputStream,
|
||||
IThreadListener listener,
|
||||
HttpMethodBase method)
|
||||
throws IOException {
|
||||
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
|
||||
ParcelFileDescriptor readSide = pipe[0];
|
||||
ParcelFileDescriptor writeSide = pipe[1];
|
||||
|
||||
// start the transfer thread
|
||||
new TransferThread(inputStream,
|
||||
new ParcelFileDescriptor.AutoCloseOutputStream(writeSide),
|
||||
listener,
|
||||
method)
|
||||
.start();
|
||||
|
||||
return readSide;
|
||||
}
|
||||
|
||||
public static class TransferThread extends Thread {
|
||||
private static final String TAG = TransferThread.class.getCanonicalName();
|
||||
private final InputStream inputStream;
|
||||
private final OutputStream outputStream;
|
||||
private final IThreadListener threadListener;
|
||||
private final HttpMethodBase httpMethod;
|
||||
|
||||
TransferThread(InputStream in, OutputStream out, IThreadListener listener, HttpMethodBase method) {
|
||||
super("ParcelFileDescriptor Transfer Thread");
|
||||
inputStream = in;
|
||||
outputStream = out;
|
||||
threadListener = listener;
|
||||
httpMethod = method;
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
|
||||
try {
|
||||
while ((len = inputStream.read(buf)) > 0) {
|
||||
outputStream.write(buf, 0, len);
|
||||
}
|
||||
outputStream.flush(); // just to be safe
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(TAG, "writing failed: " + e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(TAG, e.getMessage());
|
||||
}
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (threadListener != null) {
|
||||
threadListener.onThreadFinished(this);
|
||||
}
|
||||
|
||||
if (httpMethod != null) {
|
||||
Log_OC.i(TAG, "releaseConnection");
|
||||
httpMethod.releaseConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.appReview
|
||||
|
||||
data class AppReviewShownModel(
|
||||
var firstShowYear: String?,
|
||||
var appRestartCount: Int,
|
||||
var reviewShownCount: Int,
|
||||
var lastReviewShownDate: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.appReview
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
interface InAppReviewHelper {
|
||||
|
||||
/**
|
||||
* method to be called from Application onCreate() method to work properly
|
||||
* since we have to capture the app restarts Application is the best place to do that
|
||||
* this method will do the following:
|
||||
* 1. Reset the @see AppReviewModel with the current year (yyyy),
|
||||
* if the app is launched first time or if the year has changed.
|
||||
* 2. If the year is same then it will only increment the appRestartCount
|
||||
*/
|
||||
fun resetAndIncrementAppRestartCounter()
|
||||
|
||||
/**
|
||||
* method to be called from Activity onResume() method
|
||||
* this method will check the following conditions:
|
||||
* 1. if the minimum app restarts happened
|
||||
* 2. if the year is current
|
||||
* 3. if maximum review dialog is shown or not
|
||||
* once all the conditions satisfies it will trigger In-App Review manager to show the flow
|
||||
*/
|
||||
fun showInAppReview(activity: AppCompatActivity)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.appReview
|
||||
|
||||
import com.nextcloud.android.appReview.InAppReviewHelperImpl
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class InAppReviewModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun providesInAppReviewHelper(appPreferences: AppPreferences): InAppReviewHelper {
|
||||
return InAppReviewHelperImpl(appPreferences)
|
||||
}
|
||||
}
|
||||
83
app/src/main/java/com/nextcloud/client/NominatimClient.kt
Normal file
83
app/src/main/java/com/nextcloud/client/NominatimClient.kt
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 ZetaTom
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.owncloud.android.MainApp
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.internal.http.HTTP_OK
|
||||
import java.net.URLEncoder
|
||||
|
||||
class NominatimClient constructor(geocoderBaseUrl: String, email: String) {
|
||||
private val client = OkHttpClient()
|
||||
private val gson = Gson()
|
||||
private val reverseUrl = "${geocoderBaseUrl}reverse?format=jsonv2&email=${URLEncoder.encode(email, ENCODING_UTF_8)}"
|
||||
|
||||
private fun doRequest(requestUrl: String): String? {
|
||||
val request = Request.Builder().url(requestUrl).header(HEADER_USER_AGENT, MainApp.getUserAgent()).build()
|
||||
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.code == HTTP_OK) {
|
||||
return response.body.string()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse geocode specified location - get human readable name suitable for displaying from given coordinates.
|
||||
*
|
||||
* @param latitude GPS latitude
|
||||
* @param longitude GPS longitude
|
||||
* @param zoom level of detail to request
|
||||
*/
|
||||
fun reverseGeocode(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
zoom: ZoomLevel = ZoomLevel.TOWN_BOROUGH
|
||||
): ReverseGeocodingResult? {
|
||||
val response = doRequest("$reverseUrl&addressdetails=0&zoom=${zoom.int}&lat=$latitude&lon=$longitude")
|
||||
return response?.let { gson.fromJson(it, ReverseGeocodingResult::class.java) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ENCODING_UTF_8 = "UTF-8"
|
||||
private const val HEADER_USER_AGENT = "User-Agent"
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class ZoomLevel(val int: Int) {
|
||||
COUNTRY(3),
|
||||
STATE(5),
|
||||
COUNTY(8),
|
||||
CITY(10),
|
||||
TOWN_BOROUGH(12),
|
||||
VILLAGE_SUBURB(13),
|
||||
NEIGHBOURHOOD(14),
|
||||
LOCALITY(15),
|
||||
MAJOR_STREETS(16),
|
||||
MINOR_STREETS(17),
|
||||
BUILDING(18),
|
||||
MAX(19)
|
||||
}
|
||||
|
||||
data class ReverseGeocodingResult(
|
||||
@SerializedName("lat")
|
||||
val latitude: Double,
|
||||
@SerializedName("lon")
|
||||
val longitude: Double,
|
||||
val name: String,
|
||||
@SerializedName("display_name")
|
||||
val displayName: String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount
|
||||
import com.owncloud.android.lib.common.OwnCloudBasicCredentials
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* This object represents anonymous user, ie. user that did not log in the Nextcloud server.
|
||||
* It serves as a semantically correct "empty value", allowing simplification of logic
|
||||
* in various components requiring user data, such as DB queries.
|
||||
*/
|
||||
internal data class AnonymousUser(private val accountType: String) : User, Parcelable {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromContext(context: Context): AnonymousUser {
|
||||
val type = context.getString(R.string.account_type)
|
||||
return AnonymousUser(type)
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<AnonymousUser> = object : Parcelable.Creator<AnonymousUser> {
|
||||
override fun createFromParcel(source: Parcel): AnonymousUser = AnonymousUser(source)
|
||||
override fun newArray(size: Int): Array<AnonymousUser?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(source: Parcel) : this(
|
||||
source.readString() as String
|
||||
)
|
||||
|
||||
override val accountName: String = "anonymous@nohost"
|
||||
override val server = Server(URI.create(""), MainApp.MINIMUM_SUPPORTED_SERVER_VERSION)
|
||||
override val isAnonymous = true
|
||||
|
||||
override fun toPlatformAccount(): Account {
|
||||
return Account(accountName, accountType)
|
||||
}
|
||||
|
||||
override fun toOwnCloudAccount(): OwnCloudAccount {
|
||||
return OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", ""))
|
||||
}
|
||||
|
||||
override fun nameEquals(user: User?): Boolean {
|
||||
return user?.accountName.equals(accountName, true)
|
||||
}
|
||||
|
||||
override fun nameEquals(accountName: CharSequence?): Boolean {
|
||||
return accountName?.toString().equals(this.accountType, true)
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
|
||||
writeString(accountType)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account;
|
||||
|
||||
import android.accounts.Account;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This interface provides access to currently selected user.
|
||||
*
|
||||
* @see UserAccountManager
|
||||
*/
|
||||
public interface CurrentAccountProvider {
|
||||
/**
|
||||
* Get currently active account.
|
||||
* Replaced by getUser()
|
||||
*
|
||||
* @return Currently selected {@link Account} or first valid {@link Account} registered in OS or null, if not available at all.
|
||||
*/
|
||||
@Deprecated
|
||||
@NonNull
|
||||
Account getCurrentAccount();
|
||||
|
||||
/**
|
||||
* Get currently active user profile. If there is no active user, anonymous user is returned.
|
||||
*
|
||||
* @return User profile. Profile is never null.
|
||||
*/
|
||||
@NonNull
|
||||
default User getUser() {
|
||||
return new AnonymousUser("dummy");
|
||||
}
|
||||
}
|
||||
67
app/src/main/java/com/nextcloud/client/account/MockUser.kt
Normal file
67
app/src/main/java/com/nextcloud/client/account/MockUser.kt
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount
|
||||
import com.owncloud.android.lib.common.OwnCloudBasicCredentials
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* This is a mock user object suitable for integration tests. Mocks obtained from code generators
|
||||
* such as Mockito or MockK cannot be transported in Intent extras.
|
||||
*/
|
||||
data class MockUser(override val accountName: String, val accountType: String) : User, Parcelable {
|
||||
|
||||
constructor() : this(DEFAULT_MOCK_ACCOUNT_NAME, DEFAULT_MOCK_ACCOUNT_TYPE)
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<MockUser> = object : Parcelable.Creator<MockUser> {
|
||||
override fun createFromParcel(source: Parcel): MockUser = MockUser(source)
|
||||
override fun newArray(size: Int): Array<MockUser?> = arrayOfNulls(size)
|
||||
}
|
||||
const val DEFAULT_MOCK_ACCOUNT_NAME = "mock_account_name"
|
||||
const val DEFAULT_MOCK_ACCOUNT_TYPE = "mock_account_type"
|
||||
}
|
||||
|
||||
private constructor(source: Parcel) : this(
|
||||
source.readString() as String,
|
||||
source.readString() as String
|
||||
)
|
||||
|
||||
override val server = Server(URI.create(""), MainApp.MINIMUM_SUPPORTED_SERVER_VERSION)
|
||||
override val isAnonymous = false
|
||||
|
||||
override fun toPlatformAccount(): Account {
|
||||
return Account(accountName, accountType)
|
||||
}
|
||||
|
||||
override fun toOwnCloudAccount(): OwnCloudAccount {
|
||||
return OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", ""))
|
||||
}
|
||||
|
||||
override fun nameEquals(user: User?): Boolean {
|
||||
return user?.accountName.equals(accountName, true)
|
||||
}
|
||||
|
||||
override fun nameEquals(accountName: CharSequence?): Boolean {
|
||||
return accountName?.toString().equals(this.accountType, true)
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
|
||||
writeString(accountName)
|
||||
writeString(accountType)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount
|
||||
|
||||
/**
|
||||
* This class represents normal user logged into the Nextcloud server.
|
||||
*/
|
||||
internal data class RegisteredUser(
|
||||
private val account: Account,
|
||||
private val ownCloudAccount: OwnCloudAccount,
|
||||
override val server: Server
|
||||
) : User {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<RegisteredUser> = object : Parcelable.Creator<RegisteredUser> {
|
||||
override fun createFromParcel(source: Parcel): RegisteredUser = RegisteredUser(source)
|
||||
override fun newArray(size: Int): Array<RegisteredUser?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(source: Parcel) : this(
|
||||
source.readParcelable<Account>(Account::class.java.classLoader) as Account,
|
||||
source.readParcelable<OwnCloudAccount>(OwnCloudAccount::class.java.classLoader) as OwnCloudAccount,
|
||||
source.readParcelable<Server>(Server::class.java.classLoader) as Server
|
||||
)
|
||||
|
||||
override val isAnonymous = false
|
||||
|
||||
override val accountName: String get() {
|
||||
return account.name
|
||||
}
|
||||
|
||||
override fun toPlatformAccount(): Account {
|
||||
return account
|
||||
}
|
||||
|
||||
override fun toOwnCloudAccount(): OwnCloudAccount {
|
||||
return ownCloudAccount
|
||||
}
|
||||
|
||||
override fun nameEquals(user: User?): Boolean {
|
||||
return nameEquals(user?.accountName)
|
||||
}
|
||||
|
||||
override fun nameEquals(accountName: CharSequence?): Boolean {
|
||||
return accountName?.toString().equals(this.accountName, true)
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
|
||||
writeParcelable(account, 0)
|
||||
writeParcelable(ownCloudAccount, 0)
|
||||
writeParcelable(server, 0)
|
||||
}
|
||||
}
|
||||
40
app/src/main/java/com/nextcloud/client/account/Server.kt
Normal file
40
app/src/main/java/com/nextcloud/client/account/Server.kt
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.owncloud.android.lib.resources.status.OwnCloudVersion
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* This object provides all information necessary to interact
|
||||
* with backend server.
|
||||
*/
|
||||
data class Server(val uri: URI, val version: OwnCloudVersion) : Parcelable {
|
||||
|
||||
constructor(source: Parcel) : this(
|
||||
source.readSerializable() as URI,
|
||||
source.readParcelable<Parcelable>(OwnCloudVersion::class.java.classLoader) as OwnCloudVersion
|
||||
)
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
|
||||
writeSerializable(uri)
|
||||
writeParcelable(version, 0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<Server> = object : Parcelable.Creator<Server> {
|
||||
override fun createFromParcel(source: Parcel): Server = Server(source)
|
||||
override fun newArray(size: Int): Array<Server?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
app/src/main/java/com/nextcloud/client/account/User.kt
Normal file
56
app/src/main/java/com/nextcloud/client/account/User.kt
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.os.Parcelable
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount
|
||||
|
||||
interface User : Parcelable, com.nextcloud.common.User {
|
||||
override val accountName: String
|
||||
val server: Server
|
||||
val isAnonymous: Boolean
|
||||
|
||||
/**
|
||||
* This is temporary helper method created to facilitate incremental refactoring.
|
||||
* Code using legacy platform Account can be partially converted to instantiate User
|
||||
* object and use account instance when required.
|
||||
*
|
||||
* This method calls will allow tracing code awaiting further refactoring.
|
||||
*
|
||||
* @return Account instance that is associated with this User object.
|
||||
*/
|
||||
@Deprecated("Temporary workaround")
|
||||
override fun toPlatformAccount(): Account
|
||||
|
||||
/**
|
||||
* This is temporary helper method created to facilitate incremental refactoring.
|
||||
* Code using legacy ownCloud account can be partially converted to instantiate User
|
||||
* object and use account instance when required.
|
||||
*
|
||||
* This method calls will allow tracing code awaiting further refactoring.
|
||||
*
|
||||
* @return OwnCloudAccount instance that is associated with this User object.
|
||||
*/
|
||||
@Deprecated("Temporary workaround")
|
||||
fun toOwnCloudAccount(): OwnCloudAccount
|
||||
|
||||
/**
|
||||
* Compare account names, case insensitive.
|
||||
*
|
||||
* @return true if account names are same, false otherwise
|
||||
*/
|
||||
fun nameEquals(user: User?): Boolean
|
||||
|
||||
/**
|
||||
* Compare account names, case insensitive.
|
||||
*
|
||||
* @return true if account names are same, false otherwise
|
||||
*/
|
||||
fun nameEquals(accountName: CharSequence?): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount;
|
||||
import com.owncloud.android.lib.common.accounts.AccountUtils;
|
||||
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public interface UserAccountManager extends CurrentAccountProvider {
|
||||
|
||||
int ACCOUNT_VERSION = 1;
|
||||
int ACCOUNT_VERSION_WITH_PROPER_ID = 2;
|
||||
String ACCOUNT_USES_STANDARD_PASSWORD = "ACCOUNT_USES_STANDARD_PASSWORD";
|
||||
String PENDING_FOR_REMOVAL = "PENDING_FOR_REMOVAL";
|
||||
|
||||
@Nullable
|
||||
OwnCloudAccount getCurrentOwnCloudAccount();
|
||||
|
||||
/**
|
||||
* Remove all NextCloud accounts from OS account manager.
|
||||
*/
|
||||
void removeAllAccounts();
|
||||
|
||||
/**
|
||||
* Remove registered user.
|
||||
*
|
||||
* @param user user to remove
|
||||
* @return true if account was removed successfully, false otherwise
|
||||
*/
|
||||
boolean removeUser(User user);
|
||||
|
||||
/**
|
||||
* Get configured NextCloud's user accounts.
|
||||
*
|
||||
* @return Array of accounts or empty array, if accounts are not configured.
|
||||
*/
|
||||
@NonNull
|
||||
Account[] getAccounts();
|
||||
|
||||
/**
|
||||
* Get configured nextcloud user accounts
|
||||
* @return List of users or empty list, if users are not registered.
|
||||
*/
|
||||
@NonNull
|
||||
List<User> getAllUsers();
|
||||
|
||||
/**
|
||||
* Get user with a specific account name.
|
||||
*
|
||||
* @param accountName Account name of the requested user
|
||||
* @return User or empty optional if user does not exist.
|
||||
*/
|
||||
@NonNull
|
||||
Optional<User> getUser(CharSequence accountName);
|
||||
|
||||
|
||||
User getAnonymousUser();
|
||||
|
||||
/**
|
||||
* Check if Nextcloud account is registered in {@link android.accounts.AccountManager}
|
||||
*
|
||||
* @param account Account to check for
|
||||
* @return true if account is registered, false otherwise
|
||||
*/
|
||||
boolean exists(Account account);
|
||||
|
||||
/**
|
||||
* Verifies that every account has userId set and sets the user id if not.
|
||||
* This migration is idempotent and can be run multiple times until
|
||||
* all accounts are migrated.
|
||||
*
|
||||
* @return true if migration was successful, false if any account failed to be migrated
|
||||
*/
|
||||
boolean migrateUserId();
|
||||
|
||||
@Nullable
|
||||
Account getAccountByName(String name);
|
||||
|
||||
boolean setCurrentOwnCloudAccount(String accountName);
|
||||
|
||||
boolean setCurrentOwnCloudAccount(int hashCode);
|
||||
|
||||
/**
|
||||
* Access the version of the OC server corresponding to an account SAVED IN THE ACCOUNTMANAGER
|
||||
*
|
||||
* @param account ownCloud account
|
||||
* @return Version of the OC server corresponding to account, according to the data saved
|
||||
* in the system AccountManager
|
||||
*/
|
||||
@Deprecated
|
||||
@NonNull
|
||||
OwnCloudVersion getServerVersion(Account account);
|
||||
|
||||
void resetOwnCloudAccount();
|
||||
|
||||
/**
|
||||
* Checks if an account owns the file (file's ownerId is the same as account name)
|
||||
*
|
||||
* @param file File to check
|
||||
* @param account account to compare
|
||||
* @return false if ownerId is not set or owner is a different account
|
||||
*/
|
||||
@Deprecated
|
||||
boolean accountOwnsFile(OCFile file, Account account);
|
||||
|
||||
/**
|
||||
* Checks if an account owns the file (file's ownerId is the same as account name)
|
||||
*
|
||||
* @param file File to check
|
||||
* @param user user to check against
|
||||
* @return false if ownerId is not set or owner is a different account
|
||||
*/
|
||||
boolean userOwnsFile(OCFile file, User user);
|
||||
|
||||
/**
|
||||
* Extract username from account.
|
||||
* <p>
|
||||
* Full account name is in form of "username@nextcloud.domain".
|
||||
*
|
||||
* @param user user instance
|
||||
* @return User name (without domain) or null, if name cannot be extracted.
|
||||
*/
|
||||
static String getUsername(User user) {
|
||||
final String name = user.getAccountName();
|
||||
return name.substring(0, name.lastIndexOf('@'));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static String getDisplayName(User user) {
|
||||
return AccountManager.get(MainApp.getAppContext()).getUserData(user.toPlatformAccount(),
|
||||
AccountUtils.Constants.KEY_DISPLAY_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch account registration activity.
|
||||
* <p>
|
||||
* This method returns immediately. Authenticator activity will be launched asynchronously.
|
||||
*
|
||||
* @param activity Activity used to launch authenticator flow via {@link Activity#startActivity(Intent)}
|
||||
*/
|
||||
void startAccountCreation(Activity activity);
|
||||
}
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 TSI-mc
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.account;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AccountManagerFuture;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nextcloud.common.NextcloudClient;
|
||||
import com.nextcloud.utils.extensions.AccountExtensionsKt;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.authentication.AuthenticatorActivity;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount;
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
|
||||
import com.owncloud.android.lib.common.UserInfo;
|
||||
import com.owncloud.android.lib.common.accounts.AccountUtils;
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
|
||||
import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class UserAccountManagerImpl implements UserAccountManager {
|
||||
|
||||
private static final String TAG = UserAccountManagerImpl.class.getSimpleName();
|
||||
private static final String PREF_SELECT_OC_ACCOUNT = "select_oc_account";
|
||||
|
||||
private Context context;
|
||||
private final AccountManager accountManager;
|
||||
|
||||
public static UserAccountManagerImpl fromContext(Context context) {
|
||||
AccountManager am = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
|
||||
return new UserAccountManagerImpl(context, am);
|
||||
}
|
||||
|
||||
@Inject
|
||||
public UserAccountManagerImpl(
|
||||
Context context,
|
||||
AccountManager accountManager
|
||||
) {
|
||||
this.context = context;
|
||||
this.accountManager = accountManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllAccounts() {
|
||||
for (Account account : getAccounts()) {
|
||||
accountManager.removeAccount(account, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeUser(User user) {
|
||||
try {
|
||||
AccountManagerFuture<Boolean> result = accountManager.removeAccount(user.toPlatformAccount(),
|
||||
null,
|
||||
null);
|
||||
return result.getResult();
|
||||
} catch (OperationCanceledException| AuthenticatorException| IOException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Account[] getAccounts() {
|
||||
return accountManager.getAccountsByType(getAccountType());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<User> getAllUsers() {
|
||||
Account[] accounts = getAccounts();
|
||||
List<User> users = new ArrayList<>(accounts.length);
|
||||
for (Account account : accounts) {
|
||||
User user = createUserFromAccount(account);
|
||||
if (user != null) {
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(Account account) {
|
||||
Account[] nextcloudAccounts = getAccounts();
|
||||
|
||||
if (account != null && account.name != null) {
|
||||
int lastAtPos = account.name.lastIndexOf('@');
|
||||
String hostAndPort = account.name.substring(lastAtPos + 1);
|
||||
String username = account.name.substring(0, lastAtPos);
|
||||
String otherHostAndPort;
|
||||
String otherUsername;
|
||||
for (Account otherAccount : nextcloudAccounts) {
|
||||
lastAtPos = otherAccount.name.lastIndexOf('@');
|
||||
otherHostAndPort = otherAccount.name.substring(lastAtPos + 1);
|
||||
otherUsername = otherAccount.name.substring(0, lastAtPos);
|
||||
if (otherHostAndPort.equals(hostAndPort) &&
|
||||
otherUsername.equalsIgnoreCase(username)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Account getCurrentAccount() {
|
||||
Account[] ocAccounts = getAccounts();
|
||||
|
||||
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context);
|
||||
SharedPreferences appPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String accountName = appPreferences.getString(PREF_SELECT_OC_ACCOUNT, null);
|
||||
|
||||
Account defaultAccount = Arrays.stream(ocAccounts)
|
||||
.filter(account -> account.name.equals(accountName))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
// take first which is not pending for removal account as fallback
|
||||
if (defaultAccount == null) {
|
||||
defaultAccount = Arrays.stream(ocAccounts)
|
||||
.filter(account -> !arbitraryDataProvider.getBooleanValue(account.name, PENDING_FOR_REMOVAL))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
if (defaultAccount == null) {
|
||||
if (ocAccounts.length > 0) {
|
||||
defaultAccount = ocAccounts[0];
|
||||
} else {
|
||||
defaultAccount = getAnonymousAccount();
|
||||
}
|
||||
}
|
||||
|
||||
return defaultAccount;
|
||||
}
|
||||
|
||||
private Account getAnonymousAccount() {
|
||||
return new Account("Anonymous", context.getString(R.string.anonymous_account_type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary solution to convert platform account to user instance.
|
||||
* It takes null and returns null on error to ease error handling
|
||||
* in legacy code.
|
||||
*
|
||||
* @param account Account instance
|
||||
* @return User instance or null, if conversion failed
|
||||
*/
|
||||
@Nullable
|
||||
private User createUserFromAccount(@NonNull Account account) {
|
||||
if (AccountExtensionsKt.isAnonymous(account, context)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
Log_OC.d(TAG, "Context is null MainApp.getAppContext() used");
|
||||
context = MainApp.getAppContext();
|
||||
}
|
||||
|
||||
OwnCloudAccount ownCloudAccount;
|
||||
try {
|
||||
ownCloudAccount = new OwnCloudAccount(account, context);
|
||||
} catch (AccountUtils.AccountNotFoundException ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Server version
|
||||
*/
|
||||
String serverVersionStr = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_VERSION);
|
||||
OwnCloudVersion serverVersion;
|
||||
if (serverVersionStr != null) {
|
||||
serverVersion = new OwnCloudVersion(serverVersionStr);
|
||||
} else {
|
||||
serverVersion = MainApp.MINIMUM_SUPPORTED_SERVER_VERSION;
|
||||
}
|
||||
|
||||
/*
|
||||
* Server address
|
||||
*/
|
||||
String serverAddressStr = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL);
|
||||
if (serverAddressStr == null || serverAddressStr.isEmpty()) {
|
||||
return AnonymousUser.fromContext(context);
|
||||
}
|
||||
URI serverUri = URI.create(serverAddressStr); // TODO: validate
|
||||
|
||||
return new RegisteredUser(
|
||||
account,
|
||||
ownCloudAccount,
|
||||
new Server(serverUri, serverVersion)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user. If user cannot be retrieved due to data error, anonymous user is returned instead.
|
||||
*
|
||||
*
|
||||
* @return User instance
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public User getUser() {
|
||||
Account account = getCurrentAccount();
|
||||
User user = createUserFromAccount(account);
|
||||
return user != null ? user : AnonymousUser.fromContext(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Optional<User> getUser(CharSequence accountName) {
|
||||
Account account = getAccountByName(accountName.toString());
|
||||
User user = createUserFromAccount(account);
|
||||
return Optional.ofNullable(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getAnonymousUser() {
|
||||
return AnonymousUser.fromContext(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public OwnCloudAccount getCurrentOwnCloudAccount() {
|
||||
try {
|
||||
Account currentPlatformAccount = getCurrentAccount();
|
||||
return new OwnCloudAccount(currentPlatformAccount, context);
|
||||
} catch (AccountUtils.AccountNotFoundException | IllegalArgumentException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Account getAccountByName(String name) {
|
||||
for (Account account : getAccounts()) {
|
||||
if (account.name.equals(name)) {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
return getAnonymousAccount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setCurrentOwnCloudAccount(String accountName) {
|
||||
boolean result = false;
|
||||
if (accountName != null) {
|
||||
for (final Account account : getAccounts()) {
|
||||
if (accountName.equals(account.name)) {
|
||||
SharedPreferences.Editor appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
appPrefs.putString(PREF_SELECT_OC_ACCOUNT, accountName);
|
||||
appPrefs.apply();
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setCurrentOwnCloudAccount(int hashCode) {
|
||||
boolean result = false;
|
||||
if (hashCode != 0) {
|
||||
for (final User user : getAllUsers()) {
|
||||
if (hashCode == user.hashCode()) {
|
||||
SharedPreferences.Editor appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
appPrefs.putString(PREF_SELECT_OC_ACCOUNT, user.getAccountName());
|
||||
appPrefs.apply();
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
@NonNull
|
||||
public OwnCloudVersion getServerVersion(Account account) {
|
||||
OwnCloudVersion serverVersion = MainApp.MINIMUM_SUPPORTED_SERVER_VERSION;
|
||||
|
||||
if (account != null) {
|
||||
AccountManager accountMgr = AccountManager.get(MainApp.getAppContext());
|
||||
String serverVersionStr = accountMgr.getUserData(account, com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_VERSION);
|
||||
|
||||
if (serverVersionStr != null) {
|
||||
serverVersion = new OwnCloudVersion(serverVersionStr);
|
||||
}
|
||||
}
|
||||
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetOwnCloudAccount() {
|
||||
SharedPreferences.Editor appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
appPrefs.putString(PREF_SELECT_OC_ACCOUNT, null);
|
||||
appPrefs.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accountOwnsFile(OCFile file, Account account) {
|
||||
final String ownerId = file.getOwnerId();
|
||||
return TextUtils.isEmpty(ownerId) || account.name.split("@")[0].equalsIgnoreCase(ownerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean userOwnsFile(OCFile file, User user) {
|
||||
return accountOwnsFile(file, user.toPlatformAccount());
|
||||
}
|
||||
|
||||
public boolean migrateUserId() {
|
||||
Account[] ocAccounts = accountManager.getAccountsByType(MainApp.getAccountType(context));
|
||||
String userId;
|
||||
String displayName;
|
||||
GetUserInfoRemoteOperation remoteUserNameOperation = new GetUserInfoRemoteOperation();
|
||||
int failed = 0;
|
||||
for (Account account : ocAccounts) {
|
||||
String storedUserId = accountManager.getUserData(account, com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
|
||||
|
||||
if (!TextUtils.isEmpty(storedUserId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// add userId
|
||||
try {
|
||||
OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
|
||||
NextcloudClient nextcloudClient = OwnCloudClientManagerFactory
|
||||
.getDefaultSingleton()
|
||||
.getNextcloudClientFor(ocAccount, context);
|
||||
|
||||
RemoteOperationResult<UserInfo> result = remoteUserNameOperation.execute(nextcloudClient);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
UserInfo userInfo = result.getResultData();
|
||||
userId = userInfo.getId();
|
||||
displayName = userInfo.getDisplayName();
|
||||
} else {
|
||||
// skip account, try it next time
|
||||
Log_OC.e(TAG, "Error while getting username for account: " + account.name);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log_OC.e(TAG, "Error while getting username: " + e.getMessage());
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
accountManager.setUserData(account,
|
||||
com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_DISPLAY_NAME,
|
||||
displayName);
|
||||
accountManager.setUserData(account,
|
||||
com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID,
|
||||
userId);
|
||||
}
|
||||
|
||||
return failed == 0;
|
||||
}
|
||||
|
||||
private String getAccountType() {
|
||||
return context.getString(R.string.account_type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAccountCreation(final Activity activity) {
|
||||
Intent intent = new Intent(context, AuthenticatorActivity.class);
|
||||
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
22
app/src/main/java/com/nextcloud/client/appinfo/AppInfo.kt
Normal file
22
app/src/main/java/com/nextcloud/client/appinfo/AppInfo.kt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.appinfo
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* This class provides general, static information about application
|
||||
* build.
|
||||
*
|
||||
* All methods should be thread-safe.
|
||||
*/
|
||||
interface AppInfo {
|
||||
val versionName: String
|
||||
val versionCode: Int
|
||||
val isDebugBuild: Boolean
|
||||
fun getAppVersion(context: Context): String
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.appinfo
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.owncloud.android.BuildConfig
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
|
||||
class AppInfoImpl : AppInfo {
|
||||
override val versionName: String = BuildConfig.VERSION_NAME
|
||||
override val versionCode: Int = BuildConfig.VERSION_CODE
|
||||
override val isDebugBuild: Boolean = BuildConfig.DEBUG
|
||||
|
||||
override fun getAppVersion(context: Context): String {
|
||||
return try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
if (pInfo != null) {
|
||||
pInfo.versionName
|
||||
} else {
|
||||
"n/a"
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log_OC.e(this, "Trying to get packageName", e.cause)
|
||||
"n/a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.appinfo
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@Module
|
||||
class AppInfoModule {
|
||||
@Provides
|
||||
fun appInfo(): AppInfo {
|
||||
return AppInfoImpl()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.nextcloud.client.assistant.repository.AssistantRepositoryType
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.resources.assistant.model.Task
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class AssistantViewModel(
|
||||
private val repository: AssistantRepositoryType,
|
||||
private val context: WeakReference<Context>
|
||||
) : ViewModel() {
|
||||
|
||||
sealed class State {
|
||||
data object Idle : State()
|
||||
data object Loading : State()
|
||||
data class Error(val messageId: Int) : State()
|
||||
data class TaskCreated(val messageId: Int) : State()
|
||||
data class TaskDeleted(val messageId: Int) : State()
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow<State>(State.Loading)
|
||||
val state: StateFlow<State> = _state
|
||||
|
||||
private val _selectedTaskType = MutableStateFlow<TaskType?>(null)
|
||||
val selectedTaskType: StateFlow<TaskType?> = _selectedTaskType
|
||||
|
||||
private val _taskTypes = MutableStateFlow<List<TaskType>?>(null)
|
||||
val taskTypes: StateFlow<List<TaskType>?> = _taskTypes
|
||||
|
||||
private var _taskList: List<Task>? = null
|
||||
|
||||
private val _filteredTaskList = MutableStateFlow<List<Task>?>(null)
|
||||
val filteredTaskList: StateFlow<List<Task>?> = _filteredTaskList
|
||||
|
||||
init {
|
||||
fetchTaskTypes()
|
||||
fetchTaskList()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun createTask(
|
||||
input: String,
|
||||
type: String
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val result = repository.createTask(input, type)
|
||||
|
||||
val messageId = if (result.isSuccess) {
|
||||
R.string.assistant_screen_task_create_success_message
|
||||
} else {
|
||||
R.string.assistant_screen_task_create_fail_message
|
||||
}
|
||||
|
||||
_state.update {
|
||||
State.TaskCreated(messageId)
|
||||
}
|
||||
|
||||
delay(2000L)
|
||||
fetchTaskList()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTaskType(task: TaskType) {
|
||||
_selectedTaskType.update {
|
||||
filterTaskList(task.id)
|
||||
task
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchTaskTypes() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val allTaskType = context.get()?.getString(R.string.assistant_screen_all_task_type)
|
||||
val excludedIds = listOf("OCA\\ContextChat\\TextProcessing\\ContextChatTaskType")
|
||||
val result = arrayListOf(TaskType(null, allTaskType, null))
|
||||
val taskTypesResult = repository.getTaskTypes()
|
||||
|
||||
if (taskTypesResult.isSuccess) {
|
||||
val excludedTaskTypes = taskTypesResult.resultData.types.filter { item -> item.id !in excludedIds }
|
||||
result.addAll(excludedTaskTypes)
|
||||
_taskTypes.update {
|
||||
result.toList()
|
||||
}
|
||||
|
||||
selectTaskType(result.first())
|
||||
} else {
|
||||
_state.update {
|
||||
State.Error(R.string.assistant_screen_task_types_error_state_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchTaskList(appId: String = "assistant", onCompleted: () -> Unit = {}) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val result = repository.getTaskList(appId)
|
||||
if (result.isSuccess) {
|
||||
_taskList = result.resultData.tasks
|
||||
|
||||
filterTaskList(_selectedTaskType.value?.id)
|
||||
|
||||
_state.update {
|
||||
State.Idle
|
||||
}
|
||||
|
||||
onCompleted()
|
||||
} else {
|
||||
_state.update {
|
||||
State.Error(R.string.assistant_screen_task_list_error_state_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTask(id: Long) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val result = repository.deleteTask(id)
|
||||
|
||||
val messageId = if (result.isSuccess) {
|
||||
R.string.assistant_screen_task_delete_success_message
|
||||
} else {
|
||||
R.string.assistant_screen_task_delete_fail_message
|
||||
}
|
||||
|
||||
_state.update {
|
||||
State.TaskDeleted(messageId)
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
removeTaskFromList(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetState() {
|
||||
_state.update {
|
||||
State.Idle
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterTaskList(taskTypeId: String?) {
|
||||
if (taskTypeId == null) {
|
||||
_filteredTaskList.update {
|
||||
_taskList
|
||||
}
|
||||
} else {
|
||||
_filteredTaskList.update {
|
||||
_taskList?.filter { it.type == taskTypeId }
|
||||
}
|
||||
}
|
||||
|
||||
_filteredTaskList.update {
|
||||
it?.sortedByDescending { task ->
|
||||
task.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeTaskFromList(id: Long) {
|
||||
_filteredTaskList.update { currentList ->
|
||||
currentList?.filter { it.id != id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nextcloud.client.assistant.component.AddTaskAlertDialog
|
||||
import com.nextcloud.client.assistant.component.CenterText
|
||||
import com.nextcloud.client.assistant.taskTypes.TaskTypesRow
|
||||
import com.nextcloud.client.assistant.task.TaskView
|
||||
import com.nextcloud.client.assistant.repository.AssistantMockRepository
|
||||
import com.nextcloud.ui.composeActivity.ComposeActivity
|
||||
import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.resources.assistant.model.Task
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskType
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import kotlinx.coroutines.delay
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AssistantScreen(viewModel: AssistantViewModel, activity: Activity) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val selectedTaskType by viewModel.selectedTaskType.collectAsState()
|
||||
val filteredTaskList by viewModel.filteredTaskList.collectAsState()
|
||||
val taskTypes by viewModel.taskTypes.collectAsState()
|
||||
var showAddTaskAlertDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteTaskAlertDialog by remember { mutableStateOf(false) }
|
||||
var taskIdToDeleted: Long? by remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val pullRefreshState = rememberPullToRefreshState()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
if (pullRefreshState.isRefreshing) {
|
||||
LaunchedEffect(true) {
|
||||
delay(1500)
|
||||
viewModel.fetchTaskList(onCompleted = {
|
||||
pullRefreshState.endRefresh()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.nestedScroll(pullRefreshState.nestedScrollConnection)) {
|
||||
if (state == AssistantViewModel.State.Loading || pullRefreshState.isRefreshing) {
|
||||
CenterText(text = stringResource(id = R.string.assistant_screen_loading))
|
||||
} else {
|
||||
if (filteredTaskList.isNullOrEmpty()) {
|
||||
EmptyTaskList(selectedTaskType, taskTypes, viewModel)
|
||||
} else {
|
||||
AssistantContent(
|
||||
filteredTaskList!!,
|
||||
taskTypes,
|
||||
selectedTaskType,
|
||||
viewModel,
|
||||
showDeleteTaskAlertDialog = { taskId ->
|
||||
taskIdToDeleted = taskId
|
||||
showDeleteTaskAlertDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (pullRefreshState.isRefreshing) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
} else {
|
||||
LinearProgressIndicator(progress = { pullRefreshState.progress }, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
if (selectedTaskType?.name != stringResource(id = R.string.assistant_screen_all_task_type)) {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
onClick = {
|
||||
showAddTaskAlertDialog = true
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Filled.Add, "Add Task Icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScreenState(state, activity, viewModel)
|
||||
|
||||
if (showDeleteTaskAlertDialog) {
|
||||
taskIdToDeleted?.let { id ->
|
||||
SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title),
|
||||
description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description),
|
||||
dismiss = { showDeleteTaskAlertDialog = false },
|
||||
onComplete = { viewModel.deleteTask(id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddTaskAlertDialog) {
|
||||
selectedTaskType?.let { taskType ->
|
||||
AddTaskAlertDialog(
|
||||
title = taskType.name,
|
||||
description = taskType.description,
|
||||
addTask = { input ->
|
||||
taskType.id?.let {
|
||||
viewModel.createTask(input = input, type = it)
|
||||
}
|
||||
},
|
||||
dismiss = {
|
||||
showAddTaskAlertDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenState(
|
||||
state: AssistantViewModel.State,
|
||||
activity: Activity,
|
||||
viewModel: AssistantViewModel
|
||||
) {
|
||||
val messageId: Int? = when (state) {
|
||||
is AssistantViewModel.State.Error -> {
|
||||
state.messageId
|
||||
}
|
||||
|
||||
is AssistantViewModel.State.TaskCreated -> {
|
||||
state.messageId
|
||||
}
|
||||
|
||||
is AssistantViewModel.State.TaskDeleted -> {
|
||||
state.messageId
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
messageId?.let {
|
||||
DisplayUtils.showSnackMessage(
|
||||
activity,
|
||||
stringResource(id = messageId)
|
||||
)
|
||||
|
||||
viewModel.resetState()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun AssistantContent(
|
||||
taskList: List<Task>,
|
||||
taskTypes: List<TaskType>?,
|
||||
selectedTaskType: TaskType?,
|
||||
viewModel: AssistantViewModel,
|
||||
showDeleteTaskAlertDialog: (Long) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
stickyHeader {
|
||||
TaskTypesRow(selectedTaskType, data = taskTypes) { task ->
|
||||
viewModel.selectTaskType(task)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
items(taskList) { task ->
|
||||
TaskView(task, showDeleteTaskAlertDialog = { showDeleteTaskAlertDialog(task.id) })
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyTaskList(selectedTaskType: TaskType?, taskTypes: List<TaskType>?, viewModel: AssistantViewModel) {
|
||||
val text = if (selectedTaskType?.name == stringResource(id = R.string.assistant_screen_all_task_type)) {
|
||||
stringResource(id = R.string.assistant_screen_no_task_available_for_all_task_filter_text)
|
||||
} else {
|
||||
stringResource(
|
||||
id = R.string.assistant_screen_no_task_available_text,
|
||||
selectedTaskType?.name ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
TaskTypesRow(selectedTaskType, data = taskTypes) { task ->
|
||||
viewModel.selectTaskType(task)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
CenterText(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun AssistantScreenPreview() {
|
||||
val mockRepository = AssistantMockRepository()
|
||||
MaterialTheme(
|
||||
content = {
|
||||
AssistantScreen(
|
||||
viewModel = AssistantViewModel(
|
||||
repository = mockRepository,
|
||||
context = WeakReference(LocalContext.current)
|
||||
),
|
||||
activity = ComposeActivity()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun AssistantEmptyScreenPreview() {
|
||||
val mockRepository = AssistantMockRepository(giveEmptyTasks = true)
|
||||
MaterialTheme(
|
||||
content = {
|
||||
AssistantScreen(
|
||||
viewModel = AssistantViewModel(
|
||||
repository = mockRepository,
|
||||
context = WeakReference(LocalContext.current)
|
||||
),
|
||||
activity = ComposeActivity()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
|
||||
import com.owncloud.android.R
|
||||
|
||||
@Composable
|
||||
fun AddTaskAlertDialog(title: String?, description: String?, addTask: (String) -> Unit, dismiss: () -> Unit) {
|
||||
var input by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
|
||||
SimpleAlertDialog(
|
||||
title = title ?: "",
|
||||
description = description ?: "",
|
||||
dismiss = { dismiss() },
|
||||
onComplete = {
|
||||
addTask(input)
|
||||
},
|
||||
content = {
|
||||
TextField(
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.assistant_screen_create_task_alert_dialog_input_field_placeholder
|
||||
)
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
value = input,
|
||||
onValueChange = {
|
||||
input = it
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun AddTaskAlertDialogPreview() {
|
||||
AddTaskAlertDialog(title = "Title", description = "Description", addTask = { }, dismiss = {})
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun CenterText(text: String) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud
|
||||
* contributors
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.assistant.extensions
|
||||
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.resources.assistant.model.Task
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun Task.statusData(): Pair<Int, Int> {
|
||||
return when (status) {
|
||||
0L -> {
|
||||
Pair(R.drawable.ic_unknown, R.string.assistant_screen_unknown_task_status_text)
|
||||
}
|
||||
1L -> {
|
||||
Pair(R.drawable.ic_clock, R.string.assistant_screen_scheduled_task_status_text)
|
||||
}
|
||||
2L -> {
|
||||
Pair(R.drawable.ic_modification_desc, R.string.assistant_screen_running_task_text)
|
||||
}
|
||||
3L -> {
|
||||
Pair(R.drawable.ic_info, R.string.assistant_screen_successful_task_text)
|
||||
}
|
||||
4L -> {
|
||||
Pair(R.drawable.image_fail, R.string.assistant_screen_failed_task_text)
|
||||
}
|
||||
else -> {
|
||||
Pair(R.drawable.ic_unknown, R.string.assistant_screen_unknown_task_status_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add
|
||||
fun Task.completionDateRepresentation(): String {
|
||||
return completionExpectedAt ?: "TODO IMPLEMENT IT"
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant.repository
|
||||
|
||||
import com.nextcloud.utils.extensions.getRandomString
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.resources.assistant.model.Task
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskList
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskType
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskTypes
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class AssistantMockRepository(private val giveEmptyTasks: Boolean = false) : AssistantRepositoryType {
|
||||
override fun getTaskTypes(): RemoteOperationResult<TaskTypes> {
|
||||
return RemoteOperationResult<TaskTypes>(RemoteOperationResult.ResultCode.OK).apply {
|
||||
resultData = TaskTypes(
|
||||
listOf(
|
||||
TaskType("1", "FreePrompt", "You can create free prompt text"),
|
||||
TaskType("2", "Generate Headline", "You can create generate headline text")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createTask(input: String, type: String): RemoteOperationResult<Void> {
|
||||
return RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
|
||||
}
|
||||
|
||||
override fun getTaskList(appId: String): RemoteOperationResult<TaskList> {
|
||||
val taskList = if (giveEmptyTasks) {
|
||||
TaskList(listOf())
|
||||
} else {
|
||||
TaskList(
|
||||
listOf(
|
||||
Task(
|
||||
1,
|
||||
"FreePrompt",
|
||||
null,
|
||||
"12",
|
||||
"",
|
||||
"Give me some long text 1",
|
||||
"Lorem ipsum".getRandomString(100),
|
||||
""
|
||||
),
|
||||
Task(
|
||||
2,
|
||||
"GenerateHeadline",
|
||||
null,
|
||||
"12",
|
||||
"",
|
||||
"Give me some text 2",
|
||||
"Lorem".getRandomString(100),
|
||||
"",
|
||||
""
|
||||
),
|
||||
Task(
|
||||
3,
|
||||
"FreePrompt",
|
||||
null,
|
||||
"12",
|
||||
"",
|
||||
"Give me some text 3",
|
||||
"Lorem".getRandomString(300),
|
||||
"",
|
||||
""
|
||||
),
|
||||
Task(
|
||||
4,
|
||||
"FreePrompt",
|
||||
null,
|
||||
"12",
|
||||
"",
|
||||
"Give me some text 4",
|
||||
"Lorem".getRandomString(300),
|
||||
"",
|
||||
""
|
||||
),
|
||||
Task(
|
||||
5,
|
||||
"FreePrompt",
|
||||
null,
|
||||
"12",
|
||||
"",
|
||||
"Give me some text 5",
|
||||
"Lorem".getRandomString(300),
|
||||
"",
|
||||
""
|
||||
),
|
||||
Task(
|
||||
6,
|
||||
"FreePrompt",
|
||||
null,
|
||||
"12",
|
||||
"",
|
||||
"Give me some text 6",
|
||||
"Lorem".getRandomString(300),
|
||||
"",
|
||||
""
|
||||
),
|
||||
Task(
|
||||
7,
|
||||
"FreePrompt",
|
||||
null,
|
||||
"12",
|
||||
"",
|
||||
"Give me some text 7",
|
||||
"Lorem".getRandomString(300),
|
||||
"",
|
||||
""
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return RemoteOperationResult<TaskList>(RemoteOperationResult.ResultCode.OK).apply {
|
||||
resultData = taskList
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteTask(id: Long): RemoteOperationResult<Void> {
|
||||
return RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant.repository
|
||||
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.resources.assistant.CreateTaskRemoteOperation
|
||||
import com.owncloud.android.lib.resources.assistant.DeleteTaskRemoteOperation
|
||||
import com.owncloud.android.lib.resources.assistant.GetTaskListRemoteOperation
|
||||
import com.owncloud.android.lib.resources.assistant.GetTaskTypesRemoteOperation
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskList
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskTypes
|
||||
|
||||
class AssistantRepository(private val client: NextcloudClient) : AssistantRepositoryType {
|
||||
|
||||
override fun getTaskTypes(): RemoteOperationResult<TaskTypes> {
|
||||
return GetTaskTypesRemoteOperation().execute(client)
|
||||
}
|
||||
|
||||
override fun createTask(
|
||||
input: String,
|
||||
type: String
|
||||
): RemoteOperationResult<Void> {
|
||||
return CreateTaskRemoteOperation(input, type).execute(client)
|
||||
}
|
||||
|
||||
override fun getTaskList(appId: String): RemoteOperationResult<TaskList> {
|
||||
return GetTaskListRemoteOperation(appId).execute(client)
|
||||
}
|
||||
|
||||
override fun deleteTask(id: Long): RemoteOperationResult<Void> {
|
||||
return DeleteTaskRemoteOperation(id).execute(client)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant.repository
|
||||
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskList
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskTypes
|
||||
|
||||
interface AssistantRepositoryType {
|
||||
fun getTaskTypes(): RemoteOperationResult<TaskTypes>
|
||||
|
||||
fun createTask(
|
||||
input: String,
|
||||
type: String
|
||||
): RemoteOperationResult<Void>
|
||||
|
||||
fun getTaskList(appId: String): RemoteOperationResult<TaskList>
|
||||
|
||||
fun deleteTask(id: Long): RemoteOperationResult<Void>
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.assistant.task
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nextcloud.client.assistant.extensions.statusData
|
||||
import com.owncloud.android.lib.resources.assistant.model.Task
|
||||
|
||||
@Composable
|
||||
fun TaskStatus(task: Task, foregroundColor: Color) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val (iconId, descriptionId) = task.statusData()
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = iconId),
|
||||
modifier = Modifier.size(16.dp),
|
||||
colorFilter = ColorFilter.tint(foregroundColor),
|
||||
contentDescription = "status icon"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
Text(text = stringResource(id = descriptionId), color = foregroundColor)
|
||||
|
||||
/*
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Text(text = task.completionDateRepresentation(), color = foregroundColor)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant.task
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.nextcloud.client.assistant.taskDetail.TaskDetailBottomSheet
|
||||
import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet
|
||||
import com.nextcloud.utils.extensions.getRandomString
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.resources.assistant.model.Task
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
fun TaskView(
|
||||
task: Task,
|
||||
showDeleteTaskAlertDialog: (Long) -> Unit
|
||||
) {
|
||||
var showTaskDetailBottomSheet by remember { mutableStateOf(false) }
|
||||
var showMoreActionsBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.combinedClickable(onClick = {
|
||||
showTaskDetailBottomSheet = true
|
||||
}, onLongClick = {
|
||||
showMoreActionsBottomSheet = true
|
||||
})
|
||||
.padding(start = 8.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
task.input?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = Color.White,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
task.output?.let {
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp))
|
||||
|
||||
Text(
|
||||
text = it.take(100),
|
||||
fontSize = 12.sp,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.height(100.dp)
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
TaskStatus(task, foregroundColor = Color.White)
|
||||
|
||||
if (showMoreActionsBottomSheet) {
|
||||
val bottomSheetAction = listOf(
|
||||
Triple(
|
||||
R.drawable.ic_delete,
|
||||
R.string.assistant_screen_task_more_actions_bottom_sheet_delete_action
|
||||
) {
|
||||
showDeleteTaskAlertDialog(task.id)
|
||||
}
|
||||
)
|
||||
|
||||
MoreActionsBottomSheet(
|
||||
title = task.input,
|
||||
actions = bottomSheetAction,
|
||||
dismiss = { showMoreActionsBottomSheet = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showTaskDetailBottomSheet) {
|
||||
TaskDetailBottomSheet(task) {
|
||||
showTaskDetailBottomSheet = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TaskViewPreview() {
|
||||
val output = "Lorem".getRandomString(100)
|
||||
|
||||
TaskView(
|
||||
task = Task(
|
||||
1,
|
||||
"Free Prompt",
|
||||
0,
|
||||
"1",
|
||||
"1",
|
||||
"Give me text",
|
||||
output,
|
||||
"",
|
||||
""
|
||||
)
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.assistant.taskDetail
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.nextcloud.client.assistant.task.TaskStatus
|
||||
import com.nextcloud.utils.extensions.getRandomString
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.resources.assistant.model.Task
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TaskDetailBottomSheet(task: Task, dismiss: () -> Unit) {
|
||||
var showInput by remember { mutableStateOf(true) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier.padding(top = 32.dp),
|
||||
containerColor = Color.White,
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
sheetState = sheetState
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = colorResource(id = R.color.light_grey), shape = RoundedCornerShape(8.dp))
|
||||
) {
|
||||
TextInputSelectButton(
|
||||
Modifier.weight(1f),
|
||||
R.string.assistant_task_detail_screen_input_button_title,
|
||||
showInput,
|
||||
onClick = {
|
||||
showInput = true
|
||||
}
|
||||
)
|
||||
|
||||
TextInputSelectButton(
|
||||
Modifier.weight(1f),
|
||||
R.string.assistant_task_detail_screen_output_button_title,
|
||||
!showInput,
|
||||
onClick = {
|
||||
showInput = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = colorResource(id = R.color.light_grey), shape = RoundedCornerShape(8.dp))
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (showInput) {
|
||||
task.input ?: ""
|
||||
} else {
|
||||
task.output ?: ""
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
color = Color.Black,
|
||||
modifier = Modifier
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
TaskStatus(task, foregroundColor = Color.Black)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextInputSelectButton(modifier: Modifier, titleId: Int, highlightCondition: Boolean, onClick: () -> Unit) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = if (highlightCondition) {
|
||||
ButtonDefaults.buttonColors(containerColor = Color.White)
|
||||
} else {
|
||||
ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.light_grey))
|
||||
},
|
||||
modifier = modifier
|
||||
.widthIn(min = 0.dp, max = 200.dp)
|
||||
.padding(horizontal = 4.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = titleId), color = Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TaskDetailScreenPreview() {
|
||||
TaskDetailBottomSheet(
|
||||
task = Task(
|
||||
1,
|
||||
"Free Prompt",
|
||||
0,
|
||||
"1",
|
||||
"1",
|
||||
"Give me text".getRandomString(100),
|
||||
"output".getRandomString(300),
|
||||
"",
|
||||
""
|
||||
)
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.assistant.taskTypes
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.owncloud.android.lib.resources.assistant.model.TaskType
|
||||
|
||||
@Composable
|
||||
fun TaskTypesRow(selectedTaskType: TaskType?, data: List<TaskType>?, selectTaskType: (TaskType) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
) {
|
||||
data?.forEach { taskType ->
|
||||
taskType.name?.let { taskTypeName ->
|
||||
FilledTonalButton(
|
||||
onClick = { selectTaskType(taskType) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (selectedTaskType?.id == taskType.id) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
Color.Gray
|
||||
}
|
||||
)
|
||||
) {
|
||||
Text(text = taskTypeName)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.padding(end = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/src/main/java/com/nextcloud/client/core/AsyncRunner.kt
Normal file
58
app/src/main/java/com/nextcloud/client/core/AsyncRunner.kt
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
typealias OnResultCallback<T> = (result: T) -> Unit
|
||||
typealias OnErrorCallback = (error: Throwable) -> Unit
|
||||
typealias OnProgressCallback<P> = (progress: P) -> Unit
|
||||
typealias IsCancelled = () -> Boolean
|
||||
typealias TaskFunction<RESULT, PROGRESS> = (
|
||||
onProgress: OnProgressCallback<PROGRESS>,
|
||||
isCancelled: IsCancelled
|
||||
) -> RESULT
|
||||
|
||||
/**
|
||||
* This interface allows to post background tasks that report results via callbacks invoked on main thread.
|
||||
* It is provided as an alternative for heavy, platform specific and virtually untestable [android.os.AsyncTask]
|
||||
*
|
||||
* Please note that as of Android R, [android.os.AsyncTask] is deprecated and [java.util.concurrent] is a recommended
|
||||
* alternative.
|
||||
*/
|
||||
interface AsyncRunner {
|
||||
|
||||
/**
|
||||
* Post a quick background task and return immediately returning task cancellation interface.
|
||||
*
|
||||
* Quick task is a short piece of code that does not support interruption nor progress monitoring.
|
||||
*
|
||||
* @param task Task function returning result T; error shall be signalled by throwing an exception.
|
||||
* @param onResult Callback called when task function returns a result.
|
||||
* @param onError Callback called when task function throws an exception.
|
||||
* @return Cancellable interface, allowing cancellation of a running task.
|
||||
*/
|
||||
fun <RESULT> postQuickTask(
|
||||
task: () -> RESULT,
|
||||
onResult: OnResultCallback<RESULT>? = null,
|
||||
onError: OnErrorCallback? = null
|
||||
): Cancellable
|
||||
|
||||
/**
|
||||
* Post a background task and return immediately returning task cancellation interface.
|
||||
*
|
||||
* @param task Task function returning result T; error shall be signalled by throwing an exception.
|
||||
* @param onResult Callback called when task function returns a result,
|
||||
* @param onError Callback called when task function throws an exception.
|
||||
* @param onProgress Callback called when task function reports progress update.
|
||||
* @return Cancellable interface, allowing cancellation of a running task.
|
||||
*/
|
||||
fun <RESULT, PROGRESS> postTask(
|
||||
task: TaskFunction<RESULT, PROGRESS>,
|
||||
onResult: OnResultCallback<RESULT>? = null,
|
||||
onError: OnErrorCallback? = null,
|
||||
onProgress: OnProgressCallback<PROGRESS>? = null
|
||||
): Cancellable
|
||||
}
|
||||
25
app/src/main/java/com/nextcloud/client/core/Cancellable.kt
Normal file
25
app/src/main/java/com/nextcloud/client/core/Cancellable.kt
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
/**
|
||||
* Interface allowing cancellation of a running task.
|
||||
* Once must be careful when cancelling a non-idempotent task,
|
||||
* as cancellation does not guarantee a task termination.
|
||||
* One trivial case would be a task finished and cancelled
|
||||
* before result delivery.
|
||||
*
|
||||
* @see [com.nextcloud.client.core.AsyncRunner]
|
||||
*/
|
||||
interface Cancellable {
|
||||
|
||||
/**
|
||||
* Cancel running task. Task termination is not guaranteed, as some
|
||||
* tasks cannot be interrupted, but the result will not be delivered.
|
||||
*/
|
||||
fun cancel()
|
||||
}
|
||||
17
app/src/main/java/com/nextcloud/client/core/Clock.kt
Normal file
17
app/src/main/java/com/nextcloud/client/core/Clock.kt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
interface Clock {
|
||||
val currentTime: Long
|
||||
val currentDate: Date
|
||||
val millisSinceBoot: Long
|
||||
val tz: TimeZone
|
||||
}
|
||||
25
app/src/main/java/com/nextcloud/client/core/ClockImpl.kt
Normal file
25
app/src/main/java/com/nextcloud/client/core/ClockImpl.kt
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import android.os.SystemClock
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
class ClockImpl : Clock {
|
||||
override val currentTime: Long
|
||||
get() = System.currentTimeMillis()
|
||||
|
||||
override val currentDate: Date
|
||||
get() = Date(currentTime)
|
||||
|
||||
override val millisSinceBoot: Long
|
||||
get() = SystemClock.elapsedRealtime()
|
||||
|
||||
override val tz: TimeZone
|
||||
get() = TimeZone.getDefault()
|
||||
}
|
||||
15
app/src/main/java/com/nextcloud/client/core/LocalBinder.kt
Normal file
15
app/src/main/java/com/nextcloud/client/core/LocalBinder.kt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import android.app.Service
|
||||
import android.os.Binder
|
||||
|
||||
/**
|
||||
* This is a generic binder that provides access to a locally bound service instance.
|
||||
*/
|
||||
abstract class LocalBinder<S : Service>(val service: S) : Binder()
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import android.app.Service
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
|
||||
/**
|
||||
* This is a local service connection providing a foundation for service
|
||||
* communication logic.
|
||||
*
|
||||
* One can subclass it to create own service interaction API.
|
||||
*/
|
||||
abstract class LocalConnection<S : Service>(
|
||||
protected val context: Context
|
||||
) : ServiceConnection {
|
||||
|
||||
private var serviceBinder: LocalBinder<S>? = null
|
||||
val service: S? get() = serviceBinder?.service
|
||||
val isConnected: Boolean get() {
|
||||
return serviceBinder != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to create custom binding intent.
|
||||
* Default implementation returns null, which disables binding.
|
||||
*
|
||||
* @see [bind]
|
||||
*/
|
||||
protected open fun createBindIntent(): Intent? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind local service. If [createBindIntent] returns null, it no-ops.
|
||||
*/
|
||||
fun bind() {
|
||||
createBindIntent()?.let {
|
||||
context.bindService(it, this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind service if it is bound.
|
||||
* If service is not bound, it no-ops.
|
||||
*/
|
||||
fun unbind() {
|
||||
if (isConnected) {
|
||||
onUnbind()
|
||||
context.unbindService(this)
|
||||
serviceBinder = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when connection binds to a service.
|
||||
* Any actions taken on service connection can be taken here.
|
||||
*/
|
||||
protected open fun onBound(binder: IBinder) {
|
||||
// default no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when service is about to be unbound.
|
||||
* Binder is still valid at this stage and can be used to
|
||||
* perform cleanups. After exiting this method, service will
|
||||
* no longer be available.
|
||||
*/
|
||||
protected open fun onUnbind() {
|
||||
// default no-op
|
||||
}
|
||||
|
||||
final override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
if (binder !is LocalBinder<*>) {
|
||||
throw IllegalStateException("Binder is not extending ${LocalBinder::class.java.name}")
|
||||
}
|
||||
serviceBinder = binder as LocalBinder<S>
|
||||
onBound(binder)
|
||||
}
|
||||
|
||||
final override fun onServiceDisconnected(name: ComponentName) {
|
||||
serviceBinder = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import java.util.ArrayDeque
|
||||
|
||||
/**
|
||||
* This async runner is suitable for tests, where manual simulation of
|
||||
* asynchronous operations is desirable.
|
||||
*/
|
||||
class ManualAsyncRunner : AsyncRunner {
|
||||
|
||||
private val queue: ArrayDeque<Runnable> = ArrayDeque()
|
||||
|
||||
override fun <T> postQuickTask(
|
||||
task: () -> T,
|
||||
onResult: OnResultCallback<T>?,
|
||||
onError: OnErrorCallback?
|
||||
): Cancellable {
|
||||
return postTask(
|
||||
task = { _: OnProgressCallback<Any>, _: IsCancelled -> task.invoke() },
|
||||
onResult = onResult,
|
||||
onError = onError,
|
||||
onProgress = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun <T, P> postTask(
|
||||
task: TaskFunction<T, P>,
|
||||
onResult: OnResultCallback<T>?,
|
||||
onError: OnErrorCallback?,
|
||||
onProgress: OnProgressCallback<P>?
|
||||
): Cancellable {
|
||||
val remove: Function1<Runnable, Boolean> = queue::remove
|
||||
val taskWrapper = Task(
|
||||
postResult = {
|
||||
it.run()
|
||||
true
|
||||
},
|
||||
removeFromQueue = remove,
|
||||
taskBody = task,
|
||||
onSuccess = onResult,
|
||||
onError = onError,
|
||||
onProgress = onProgress
|
||||
)
|
||||
queue.push(taskWrapper)
|
||||
return taskWrapper
|
||||
}
|
||||
|
||||
val size: Int get() = queue.size
|
||||
val isEmpty: Boolean get() = queue.size == 0
|
||||
|
||||
/**
|
||||
* Run all enqueued tasks until queue is empty. This will run also tasks
|
||||
* enqueued by task callbacks.
|
||||
*
|
||||
* @param maximum max number of tasks to run to avoid infinite loopss
|
||||
* @return number of executed tasks
|
||||
*/
|
||||
fun runAll(maximum: Int = 100): Int {
|
||||
var c = 0
|
||||
while (queue.size > 0) {
|
||||
val t = queue.remove()
|
||||
t.run()
|
||||
c++
|
||||
if (c > maximum) {
|
||||
throw IllegalStateException("Maximum number of tasks run. Are you in infinite loop?")
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one pending task
|
||||
*
|
||||
* @return true if task has been run
|
||||
*/
|
||||
fun runOne(): Boolean {
|
||||
val t = queue.pollFirst()
|
||||
t?.run()
|
||||
return t != null
|
||||
}
|
||||
}
|
||||
57
app/src/main/java/com/nextcloud/client/core/Task.kt
Normal file
57
app/src/main/java/com/nextcloud/client/core/Task.kt
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* This is a wrapper for a task function running in background.
|
||||
* It executes task function and handles result or error delivery.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class Task<T, P>(
|
||||
private val postResult: (Runnable) -> Boolean,
|
||||
private val removeFromQueue: (Runnable) -> Boolean,
|
||||
private val taskBody: TaskFunction<T, P>,
|
||||
private val onSuccess: OnResultCallback<T>?,
|
||||
private val onError: OnErrorCallback?,
|
||||
private val onProgress: OnProgressCallback<P>?
|
||||
) : Runnable, Cancellable {
|
||||
|
||||
val isCancelled: Boolean
|
||||
get() = cancelled.get()
|
||||
|
||||
private val cancelled = AtomicBoolean(false)
|
||||
|
||||
private fun postProgress(p: P) {
|
||||
postResult(Runnable { onProgress?.invoke(p) })
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught") // this is exactly what we want here
|
||||
override fun run() {
|
||||
try {
|
||||
val result = taskBody.invoke({ postProgress(it) }, this::isCancelled)
|
||||
if (!cancelled.get()) {
|
||||
postResult.invoke(
|
||||
Runnable {
|
||||
onSuccess?.invoke(result)
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
if (!cancelled.get()) {
|
||||
postResult(Runnable { onError?.invoke(t) })
|
||||
}
|
||||
}
|
||||
removeFromQueue(this)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
cancelled.set(true)
|
||||
removeFromQueue(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import android.os.Handler
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor
|
||||
|
||||
/**
|
||||
* This async runner uses [java.util.concurrent.ScheduledThreadPoolExecutor] to run tasks
|
||||
* asynchronously.
|
||||
*
|
||||
* Tasks are run on multi-threaded pool. If serialized execution is desired, set [corePoolSize] to 1.
|
||||
*/
|
||||
internal class ThreadPoolAsyncRunner(
|
||||
private val uiThreadHandler: Handler,
|
||||
corePoolSize: Int,
|
||||
val tag: String = "default"
|
||||
) : AsyncRunner {
|
||||
|
||||
private val executor = ScheduledThreadPoolExecutor(corePoolSize)
|
||||
|
||||
override fun <T> postQuickTask(
|
||||
task: () -> T,
|
||||
onResult: OnResultCallback<T>?,
|
||||
onError: OnErrorCallback?
|
||||
): Cancellable {
|
||||
val taskAdapter = { _: OnProgressCallback<Void>, _: IsCancelled -> task.invoke() }
|
||||
return postTask(
|
||||
taskAdapter,
|
||||
onResult,
|
||||
onError,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun <T, P> postTask(
|
||||
task: TaskFunction<T, P>,
|
||||
onResult: OnResultCallback<T>?,
|
||||
onError: OnErrorCallback?,
|
||||
onProgress: OnProgressCallback<P>?
|
||||
): Cancellable {
|
||||
val remove: Function1<Runnable, Boolean> = executor::remove
|
||||
val taskWrapper = Task(
|
||||
postResult = uiThreadHandler::post,
|
||||
removeFromQueue = remove,
|
||||
taskBody = task,
|
||||
onSuccess = onResult,
|
||||
onError = onError,
|
||||
onProgress = onProgress
|
||||
)
|
||||
executor.execute(taskWrapper)
|
||||
return taskWrapper
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database
|
||||
|
||||
import android.content.Context
|
||||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.database.dao.ArbitraryDataDao
|
||||
import com.nextcloud.client.database.dao.FileDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun database(context: Context, clock: Clock): NextcloudDatabase {
|
||||
return NextcloudDatabase.getInstance(context, clock)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun arbitraryDataDao(nextcloudDatabase: NextcloudDatabase): ArbitraryDataDao {
|
||||
return nextcloudDatabase.arbitraryDataDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao {
|
||||
return nextcloudDatabase.fileDao()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.core.ClockImpl
|
||||
import com.nextcloud.client.database.dao.ArbitraryDataDao
|
||||
import com.nextcloud.client.database.dao.FileDao
|
||||
import com.nextcloud.client.database.entity.ArbitraryDataEntity
|
||||
import com.nextcloud.client.database.entity.CapabilityEntity
|
||||
import com.nextcloud.client.database.entity.ExternalLinkEntity
|
||||
import com.nextcloud.client.database.entity.FileEntity
|
||||
import com.nextcloud.client.database.entity.FilesystemEntity
|
||||
import com.nextcloud.client.database.entity.ShareEntity
|
||||
import com.nextcloud.client.database.entity.SyncedFolderEntity
|
||||
import com.nextcloud.client.database.entity.UploadEntity
|
||||
import com.nextcloud.client.database.entity.VirtualEntity
|
||||
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil
|
||||
import com.nextcloud.client.database.migrations.Migration67to68
|
||||
import com.nextcloud.client.database.migrations.RoomMigration
|
||||
import com.nextcloud.client.database.migrations.addLegacyMigrations
|
||||
import com.owncloud.android.db.ProviderMeta
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
ArbitraryDataEntity::class,
|
||||
CapabilityEntity::class,
|
||||
ExternalLinkEntity::class,
|
||||
FileEntity::class,
|
||||
FilesystemEntity::class,
|
||||
ShareEntity::class,
|
||||
SyncedFolderEntity::class,
|
||||
UploadEntity::class,
|
||||
VirtualEntity::class
|
||||
],
|
||||
version = ProviderMeta.DB_VERSION,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 65, to = 66),
|
||||
AutoMigration(from = 66, to = 67),
|
||||
AutoMigration(from = 68, to = 69),
|
||||
AutoMigration(from = 69, to = 70),
|
||||
AutoMigration(from = 70, to = 71, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
|
||||
AutoMigration(from = 71, to = 72),
|
||||
AutoMigration(from = 72, to = 73),
|
||||
AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
|
||||
AutoMigration(from = 74, to = 75),
|
||||
AutoMigration(from = 75, to = 76),
|
||||
AutoMigration(from = 76, to = 77),
|
||||
AutoMigration(from = 77, to = 78),
|
||||
AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
|
||||
AutoMigration(from = 79, to = 80),
|
||||
AutoMigration(from = 80, to = 81)
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
@Suppress("Detekt.UnnecessaryAbstractClass") // needed by Room
|
||||
abstract class NextcloudDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun arbitraryDataDao(): ArbitraryDataDao
|
||||
abstract fun fileDao(): FileDao
|
||||
|
||||
companion object {
|
||||
const val FIRST_ROOM_DB_VERSION = 65
|
||||
private var INSTANCE: NextcloudDatabase? = null
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Here for legacy purposes, inject this class or use getInstance(context, clock) instead")
|
||||
fun getInstance(context: Context): NextcloudDatabase {
|
||||
return getInstance(context, ClockImpl())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(context: Context, clock: Clock): NextcloudDatabase {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = Room
|
||||
.databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME)
|
||||
.allowMainThreadQueries()
|
||||
.addLegacyMigrations(clock, context)
|
||||
.addMigrations(RoomMigration())
|
||||
.addMigrations(Migration67to68())
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
return INSTANCE!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.nextcloud.client.database.entity.ArbitraryDataEntity
|
||||
|
||||
@Dao
|
||||
interface ArbitraryDataDao {
|
||||
@Query("INSERT INTO arbitrary_data(cloud_id, `key`, value) VALUES(:accountName, :key, :value)")
|
||||
fun insertValue(accountName: String, key: String, value: String?)
|
||||
|
||||
@Query("SELECT * FROM arbitrary_data WHERE cloud_id = :accountName AND `key` = :key LIMIT 1")
|
||||
fun getByAccountAndKey(accountName: String, key: String): ArbitraryDataEntity?
|
||||
|
||||
@Query("UPDATE arbitrary_data SET value = :value WHERE cloud_id = :accountName AND `key` = :key ")
|
||||
fun updateValue(accountName: String, key: String, value: String?)
|
||||
|
||||
@Query("DELETE FROM arbitrary_data WHERE cloud_id = :accountName AND `key` = :key")
|
||||
fun deleteValue(accountName: String, key: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Dariusz Olszewski <starypatyk@users.noreply.github.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.nextcloud.client.database.entity.FileEntity
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Dao
|
||||
interface FileDao {
|
||||
@Query("SELECT * FROM filelist WHERE _id = :id LIMIT 1")
|
||||
fun getFileById(id: Long): FileEntity?
|
||||
|
||||
@Query("SELECT * FROM filelist WHERE path = :path AND file_owner = :fileOwner LIMIT 1")
|
||||
fun getFileByEncryptedRemotePath(path: String, fileOwner: String): FileEntity?
|
||||
|
||||
@Query("SELECT * FROM filelist WHERE path_decrypted = :path AND file_owner = :fileOwner LIMIT 1")
|
||||
fun getFileByDecryptedRemotePath(path: String, fileOwner: String): FileEntity?
|
||||
|
||||
@Query("SELECT * FROM filelist WHERE media_path = :path AND file_owner = :fileOwner LIMIT 1")
|
||||
fun getFileByLocalPath(path: String, fileOwner: String): FileEntity?
|
||||
|
||||
@Query("SELECT * FROM filelist WHERE remote_id = :remoteId AND file_owner = :fileOwner LIMIT 1")
|
||||
fun getFileByRemoteId(remoteId: String, fileOwner: String): FileEntity?
|
||||
|
||||
@Query("SELECT * FROM filelist WHERE parent = :parentId ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}")
|
||||
fun getFolderContent(parentId: Long): List<FileEntity>
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM filelist WHERE modified >= :startDate" +
|
||||
" AND modified < :endDate" +
|
||||
" AND (content_type LIKE 'image/%' OR content_type LIKE 'video/%')" +
|
||||
" AND file_owner = :fileOwner" +
|
||||
" ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}"
|
||||
)
|
||||
fun getGalleryItems(startDate: Long, endDate: Long, fileOwner: String): List<FileEntity>
|
||||
|
||||
@Query("SELECT * FROM filelist WHERE file_owner = :fileOwner ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}")
|
||||
fun getAllFiles(fileOwner: String): List<FileEntity>
|
||||
|
||||
@Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC")
|
||||
fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List<FileEntity>
|
||||
|
||||
@Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL")
|
||||
fun getFilesWithSyncConflict(fileOwner: String): List<FileEntity>
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME)
|
||||
data class ArbitraryDataEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID)
|
||||
val cloudId: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.ARBITRARY_DATA_KEY)
|
||||
val key: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.ARBITRARY_DATA_VALUE)
|
||||
val value: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.CAPABILITIES_TABLE_NAME)
|
||||
data class CapabilityEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ASSISTANT)
|
||||
val assistant: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME)
|
||||
val accountName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_VERSION_MAYOR)
|
||||
val versionMajor: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_VERSION_MINOR)
|
||||
val versionMinor: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_VERSION_MICRO)
|
||||
val versionMicro: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_VERSION_STRING)
|
||||
val versionString: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_VERSION_EDITION)
|
||||
val versionEditor: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_EXTENDED_SUPPORT)
|
||||
val extendedSupport: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL)
|
||||
val corePollinterval: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_API_ENABLED)
|
||||
val sharingApiEnabled: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ENABLED)
|
||||
val sharingPublicEnabled: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED)
|
||||
val sharingPublicPasswordEnforced: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED)
|
||||
val sharingPublicExpireDateEnabled: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS)
|
||||
val sharingPublicExpireDateDays: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED)
|
||||
val sharingPublicExpireDateEnforced: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_SEND_MAIL)
|
||||
val sharingPublicSendMail: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_UPLOAD)
|
||||
val sharingPublicUpload: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_USER_SEND_MAIL)
|
||||
val sharingUserSendMail: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_RESHARING)
|
||||
val sharingResharing: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_OUTGOING)
|
||||
val sharingFederationOutgoing: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_INCOMING)
|
||||
val sharingFederationIncoming: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_BIGFILECHUNKING)
|
||||
val filesBigfilechunking: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_UNDELETE)
|
||||
val filesUndelete: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_VERSIONING)
|
||||
val filesVersioning: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_EXTERNAL_LINKS)
|
||||
val externalLinks: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_NAME)
|
||||
val serverName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_COLOR)
|
||||
val serverColor: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR)
|
||||
val serverTextColor: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR)
|
||||
val serverElementColor: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN)
|
||||
val serverSlogan: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_LOGO)
|
||||
val serverLogo: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL)
|
||||
val serverBackgroundUrl: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION)
|
||||
val endToEndEncryption: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST)
|
||||
val endToEndEncryptionKeysExist: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION)
|
||||
val endToEndEncryptionApiVersion: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ACTIVITY)
|
||||
val activity: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT)
|
||||
val serverBackgroundDefault: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_PLAIN)
|
||||
val serverBackgroundPlain: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RICHDOCUMENT)
|
||||
val richdocument: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_MIMETYPE_LIST)
|
||||
val richdocumentMimetypeList: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_DIRECT_EDITING)
|
||||
val richdocumentDirectEditing: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_TEMPLATES)
|
||||
val richdocumentTemplates: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_OPTIONAL_MIMETYPE_LIST)
|
||||
val richdocumentOptionalMimetypeList: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD)
|
||||
val sharingPublicAskForOptionalPassword: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME)
|
||||
val richdocumentProductName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG)
|
||||
val directEditingEtag: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_USER_STATUS)
|
||||
val userStatus: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI)
|
||||
val userStatusSupportsEmoji: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ETAG)
|
||||
val etag: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION)
|
||||
val filesLockingVersion: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)
|
||||
val groupfolders: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)
|
||||
val dropAccount: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)
|
||||
val securityGuard: Int?
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME)
|
||||
data class ExternalLinkEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.EXTERNAL_LINKS_ICON_URL)
|
||||
val iconUrl: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.EXTERNAL_LINKS_LANGUAGE)
|
||||
val language: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.EXTERNAL_LINKS_TYPE)
|
||||
val type: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.EXTERNAL_LINKS_NAME)
|
||||
val name: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.EXTERNAL_LINKS_URL)
|
||||
val url: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.EXTERNAL_LINKS_REDIRECT)
|
||||
val redirect: Int?
|
||||
)
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.FILE_TABLE_NAME)
|
||||
data class FileEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_NAME)
|
||||
val name: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_ENCRYPTED_NAME)
|
||||
val encryptedName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_PATH)
|
||||
val path: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_PATH_DECRYPTED)
|
||||
val pathDecrypted: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_PARENT)
|
||||
val parent: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_CREATION)
|
||||
val creation: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_MODIFIED)
|
||||
val modified: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_CONTENT_TYPE)
|
||||
val contentType: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_CONTENT_LENGTH)
|
||||
val contentLength: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_STORAGE_PATH)
|
||||
val storagePath: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_ACCOUNT_OWNER)
|
||||
val accountOwner: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LAST_SYNC_DATE)
|
||||
val lastSyncDate: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA)
|
||||
val lastSyncDateForData: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA)
|
||||
val modifiedAtLastSyncForData: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_ETAG)
|
||||
val etag: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_ETAG_ON_SERVER)
|
||||
val etagOnServer: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_SHARED_VIA_LINK)
|
||||
val sharedViaLink: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_PERMISSIONS)
|
||||
val permissions: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_REMOTE_ID)
|
||||
val remoteId: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCAL_ID, defaultValue = "-1")
|
||||
val localId: Long,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_UPDATE_THUMBNAIL)
|
||||
val updateThumbnail: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_IS_DOWNLOADING)
|
||||
val isDownloading: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_FAVORITE)
|
||||
val favorite: Int?,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_HIDDEN)
|
||||
val hidden: Int?,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_IS_ENCRYPTED)
|
||||
val isEncrypted: Int?,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_ETAG_IN_CONFLICT)
|
||||
val etagInConflict: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_SHARED_WITH_SHAREE)
|
||||
val sharedWithSharee: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_MOUNT_TYPE)
|
||||
val mountType: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_HAS_PREVIEW)
|
||||
val hasPreview: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_UNREAD_COMMENTS_COUNT)
|
||||
val unreadCommentsCount: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_OWNER_ID)
|
||||
val ownerId: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_OWNER_DISPLAY_NAME)
|
||||
val ownerDisplayName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_NOTE)
|
||||
val note: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_SHAREES)
|
||||
val sharees: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_RICH_WORKSPACE)
|
||||
val richWorkspace: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_METADATA_SIZE)
|
||||
val metadataSize: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_METADATA_LIVE_PHOTO)
|
||||
val metadataLivePhoto: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCKED)
|
||||
val locked: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TYPE)
|
||||
val lockType: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_OWNER)
|
||||
val lockOwner: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_OWNER_DISPLAY_NAME)
|
||||
val lockOwnerDisplayName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_OWNER_EDITOR)
|
||||
val lockOwnerEditor: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TIMESTAMP)
|
||||
val lockTimestamp: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TIMEOUT)
|
||||
val lockTimeout: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TOKEN)
|
||||
val lockToken: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_TAGS)
|
||||
val tags: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS)
|
||||
val metadataGPS: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER)
|
||||
val e2eCounter: Long?
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.FILESYSTEM_TABLE_NAME)
|
||||
data class FilesystemEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH)
|
||||
val localPath: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER)
|
||||
val fileIsFolder: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY)
|
||||
val fileFoundRecently: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD)
|
||||
val fileSentForUpload: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID)
|
||||
val syncedFolderId: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_CRC32)
|
||||
val crc32: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_MODIFIED)
|
||||
val fileModified: Long?
|
||||
)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.OCSHARES_TABLE_NAME)
|
||||
data class ShareEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_FILE_SOURCE)
|
||||
val fileSource: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_ITEM_SOURCE)
|
||||
val itemSource: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_TYPE)
|
||||
val shareType: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_WITH)
|
||||
val shareWith: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_PATH)
|
||||
val path: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_PERMISSIONS)
|
||||
val permissions: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARED_DATE)
|
||||
val sharedDate: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_EXPIRATION_DATE)
|
||||
val expirationDate: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_TOKEN)
|
||||
val token: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME)
|
||||
val shareWithDisplayName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_IS_DIRECTORY)
|
||||
val isDirectory: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_USER_ID)
|
||||
val userId: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED)
|
||||
val idRemoteShared: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_ACCOUNT_OWNER)
|
||||
val accountOwner: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_IS_PASSWORD_PROTECTED)
|
||||
val isPasswordProtected: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_NOTE)
|
||||
val note: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_HIDE_DOWNLOAD)
|
||||
val hideDownload: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_LINK)
|
||||
val shareLink: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_LABEL)
|
||||
val shareLabel: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME)
|
||||
data class SyncedFolderEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH)
|
||||
val localPath: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH)
|
||||
val remotePath: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY)
|
||||
val wifiOnly: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY)
|
||||
val chargingOnly: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_EXISTING)
|
||||
val existing: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_ENABLED)
|
||||
val enabled: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS)
|
||||
val enabledTimestampMs: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE)
|
||||
val subfolderByDate: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_ACCOUNT)
|
||||
val account: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION)
|
||||
val uploadAction: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_NAME_COLLISION_POLICY)
|
||||
val nameCollisionPolicy: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_TYPE)
|
||||
val type: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_HIDDEN)
|
||||
val hidden: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE)
|
||||
val subFolderRule: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_EXCLUDE_HIDDEN)
|
||||
val excludeHidden: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS)
|
||||
val lastScanTimestampMs: Long?
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.UPLOADS_TABLE_NAME)
|
||||
data class UploadEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_LOCAL_PATH)
|
||||
val localPath: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_REMOTE_PATH)
|
||||
val remotePath: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_ACCOUNT_NAME)
|
||||
val accountName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_FILE_SIZE)
|
||||
val fileSize: Long?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_STATUS)
|
||||
val status: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR)
|
||||
val localBehaviour: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_UPLOAD_TIME)
|
||||
val uploadTime: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY)
|
||||
val nameCollisionPolicy: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER)
|
||||
val isCreateRemoteFolder: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP)
|
||||
val uploadEndTimestamp: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_LAST_RESULT)
|
||||
val lastResult: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY)
|
||||
val isWhileChargingOnly: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_IS_WIFI_ONLY)
|
||||
val isWifiOnly: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_CREATED_BY)
|
||||
val createdBy: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN)
|
||||
val folderUnlockToken: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.VIRTUAL_TABLE_NAME)
|
||||
data class VirtualEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int?,
|
||||
@ColumnInfo(name = ProviderTableMeta.VIRTUAL_TYPE)
|
||||
val type: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.VIRTUAL_OCFILE_ID)
|
||||
val ocFileId: Int?
|
||||
)
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.migrations
|
||||
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
object DatabaseMigrationUtil {
|
||||
|
||||
const val TYPE_TEXT = "TEXT"
|
||||
const val TYPE_INTEGER = "INTEGER"
|
||||
const val TYPE_INTEGER_PRIMARY_KEY = "INTEGER PRIMARY KEY"
|
||||
const val KEYWORD_NOT_NULL = "NOT NULL"
|
||||
|
||||
/**
|
||||
* Utility method to add or remove columns from a table
|
||||
*
|
||||
* See individual functions for more details
|
||||
*
|
||||
* @param newColumns Map of column names and types on the NEW table
|
||||
* @param selectTransform a function that transforms the select statement. This can be used to change the values
|
||||
* when copying, such as for removing nulls
|
||||
*/
|
||||
fun migrateTable(
|
||||
database: SupportSQLiteDatabase,
|
||||
tableName: String,
|
||||
newColumns: Map<String, String>,
|
||||
selectTransform: ((String) -> String)? = null
|
||||
) {
|
||||
require(newColumns.isNotEmpty())
|
||||
val newTableTempName = "${tableName}_new"
|
||||
createNewTable(database, newTableTempName, newColumns)
|
||||
copyData(database, tableName, newTableTempName, newColumns.keys, selectTransform)
|
||||
replaceTable(database, tableName, newTableTempName)
|
||||
}
|
||||
|
||||
fun resetCapabilities(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("UPDATE capabilities SET etag = '' WHERE 1=1")
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to create a new table with the given columns
|
||||
*/
|
||||
private fun createNewTable(
|
||||
database: SupportSQLiteDatabase,
|
||||
newTableName: String,
|
||||
columns: Map<String, String>
|
||||
) {
|
||||
val columnsString = columns.entries.joinToString(",") { "${it.key} ${it.value}" }
|
||||
database.execSQL("CREATE TABLE $newTableName ($columnsString)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to copy data from an old table to a new table. Only the columns in [columnNames] will be copied
|
||||
*
|
||||
* @param selectTransform a function that transforms the select statement. This can be used to change the values
|
||||
* when copying, such as for removing nulls
|
||||
*/
|
||||
private fun copyData(
|
||||
database: SupportSQLiteDatabase,
|
||||
tableName: String,
|
||||
newTableName: String,
|
||||
columnNames: Iterable<String>,
|
||||
selectTransform: ((String) -> String)? = null
|
||||
) {
|
||||
val selectColumnsString = columnNames.joinToString(",", transform = selectTransform)
|
||||
val destColumnsString = columnNames.joinToString(",")
|
||||
|
||||
database.execSQL(
|
||||
"INSERT INTO $newTableName ($destColumnsString) " +
|
||||
"SELECT $selectColumnsString FROM $tableName"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to replace an old table with a new one, essentially deleting the old one and renaming the new one
|
||||
*/
|
||||
private fun replaceTable(
|
||||
database: SupportSQLiteDatabase,
|
||||
tableName: String,
|
||||
newTableTempName: String
|
||||
) {
|
||||
database.execSQL("DROP TABLE $tableName")
|
||||
database.execSQL("ALTER TABLE $newTableTempName RENAME TO $tableName")
|
||||
}
|
||||
|
||||
/**
|
||||
* Room AutoMigrationSpec to reset capabilities post migration.
|
||||
*/
|
||||
class ResetCapabilitiesPostMigration : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
resetCapabilities(db)
|
||||
super.onPostMigrate(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.migrations
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.database.NextcloudDatabase
|
||||
|
||||
private const val MIN_SUPPORTED_DB_VERSION = 24
|
||||
|
||||
/**
|
||||
* Migrations for DB versions before Room was introduced
|
||||
*/
|
||||
class LegacyMigration(
|
||||
private val from: Int,
|
||||
private val to: Int,
|
||||
private val clock: Clock,
|
||||
private val context: Context
|
||||
) : Migration(from, to) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
LegacyMigrationHelper(clock, context)
|
||||
.tryUpgrade(database, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a legacy migration for all versions before Room was introduced
|
||||
*
|
||||
* This is needed because the [Migration] does not know which versions it's dealing with
|
||||
*/
|
||||
@Suppress("ForEachOnRange")
|
||||
fun RoomDatabase.Builder<NextcloudDatabase>.addLegacyMigrations(
|
||||
clock: Clock,
|
||||
context: Context
|
||||
): RoomDatabase.Builder<NextcloudDatabase> {
|
||||
(MIN_SUPPORTED_DB_VERSION until NextcloudDatabase.FIRST_ROOM_DB_VERSION - 1)
|
||||
.map { from -> LegacyMigration(from, from + 1, clock, context) }
|
||||
.forEach { migration -> this.addMigrations(migration) }
|
||||
return this
|
||||
}
|
||||
|
|
@ -0,0 +1,959 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.migrations;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.nextcloud.client.core.Clock;
|
||||
import com.owncloud.android.datamodel.SyncedFolder;
|
||||
import com.owncloud.android.db.ProviderMeta;
|
||||
import com.owncloud.android.files.services.NameCollisionPolicy;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
|
||||
public class LegacyMigrationHelper {
|
||||
|
||||
private static final String TAG = LegacyMigrationHelper.class.getSimpleName();
|
||||
|
||||
public static final int ARBITRARY_DATA_TABLE_INTRODUCTION_VERSION = 20;
|
||||
|
||||
|
||||
private static final String ALTER_TABLE = "ALTER TABLE ";
|
||||
private static final String ADD_COLUMN = " ADD COLUMN ";
|
||||
private static final String INTEGER = " INTEGER, ";
|
||||
private static final String TEXT = " TEXT, ";
|
||||
|
||||
private static final String UPGRADE_VERSION_MSG = "OUT of the ADD in onUpgrade; oldVersion == %d, newVersion == %d";
|
||||
|
||||
private final Clock clock;
|
||||
private final Context context;
|
||||
|
||||
public LegacyMigrationHelper(Clock clock, Context context) {
|
||||
this.clock = clock;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void tryUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
try {
|
||||
upgrade(db, oldVersion, newVersion);
|
||||
} catch (Throwable t) {
|
||||
Log_OC.i(TAG, "Migration upgrade failed due to " + t);
|
||||
clearStorage();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
|
||||
private void clearStorage() {
|
||||
context.getCacheDir().delete();
|
||||
((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).clearApplicationUserData();
|
||||
}
|
||||
|
||||
private void upgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
Log_OC.i(TAG, "Entering in onUpgrade");
|
||||
boolean upgraded = false;
|
||||
|
||||
if (oldVersion < 25 && newVersion >= 25) {
|
||||
Log_OC.i(TAG, "Entering in the #25 Adding encryption flag to file");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_IS_ENCRYPTED + " INTEGER ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_ENCRYPTED_NAME + " TEXT ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION + " INTEGER ");
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 26 && newVersion >= 26) {
|
||||
Log_OC.i(TAG, "Entering in the #26 Adding text and element color to capabilities");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR + " TEXT ");
|
||||
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 27 && newVersion >= 27) {
|
||||
Log_OC.i(TAG, "Entering in the #27 Adding token to ocUpload");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.UPLOADS_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN + " TEXT ");
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 28 && newVersion >= 28) {
|
||||
Log_OC.i(TAG, "Entering in the #28 Adding CRC32 column to filesystem table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32 + " TEXT ");
|
||||
}
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 29 && newVersion >= 29) {
|
||||
Log_OC.i(TAG, "Entering in the #29 Adding background default/plain to capabilities");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT + " INTEGER ");
|
||||
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_PLAIN + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 30 && newVersion >= 30) {
|
||||
Log_OC.i(TAG, "Entering in the #30 Re-add 25, 26 if needed");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.FILE_IS_ENCRYPTED)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_IS_ENCRYPTED + " INTEGER ");
|
||||
}
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.FILE_ENCRYPTED_NAME)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_ENCRYPTED_NAME + " TEXT ");
|
||||
}
|
||||
if (oldVersion > ARBITRARY_DATA_TABLE_INTRODUCTION_VERSION) {
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION + " INTEGER ");
|
||||
}
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR + " TEXT ");
|
||||
}
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR + " TEXT ");
|
||||
}
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32)) {
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32 + " TEXT ");
|
||||
} catch (SQLiteException e) {
|
||||
Log_OC.d(TAG, "Known problem on adding same column twice when upgrading from 24->30");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 31 && newVersion >= 31) {
|
||||
Log_OC.i(TAG, "Entering in the #31 add mount type");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_MOUNT_TYPE + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 32 && newVersion >= 32) {
|
||||
Log_OC.i(TAG, "Entering in the #32 add ocshares.is_password_protected");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.OCSHARES_IS_PASSWORD_PROTECTED + " INTEGER "); // boolean
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 33 && newVersion >= 33) {
|
||||
Log_OC.i(TAG, "Entering in the #3 Adding activity to capability");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_ACTIVITY + " INTEGER ");
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 34 && newVersion >= 34) {
|
||||
Log_OC.i(TAG, "Entering in the #34 add redirect to external links");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_REDIRECT)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_REDIRECT + " INTEGER "); // boolean
|
||||
}
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 35 && newVersion >= 35) {
|
||||
Log_OC.i(TAG, "Entering in the #35 add note to share table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.OCSHARES_NOTE + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 36 && newVersion >= 36) {
|
||||
Log_OC.i(TAG, "Entering in the #36 add has-preview to file table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_HAS_PREVIEW + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 37 && newVersion >= 37) {
|
||||
Log_OC.i(TAG, "Entering in the #37 add hide-download to share table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.OCSHARES_HIDE_DOWNLOAD + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 38 && newVersion >= 38) {
|
||||
Log_OC.i(TAG, "Entering in the #38 add richdocuments");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_RICHDOCUMENT + " INTEGER "); // boolean
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_MIMETYPE_LIST + " TEXT "); // string
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 39 && newVersion >= 39) {
|
||||
Log_OC.i(TAG, "Entering in the #39 add richdocuments direct editing");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_DIRECT_EDITING + " INTEGER "); // bool
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 40 && newVersion >= 40) {
|
||||
Log_OC.i(TAG, "Entering in the #40 add unreadCommentsCount to file table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_UNREAD_COMMENTS_COUNT + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 41 && newVersion >= 41) {
|
||||
Log_OC.i(TAG, "Entering in the #41 add eTagOnServer");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_ETAG_ON_SERVER + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 42 && newVersion >= 42) {
|
||||
Log_OC.i(TAG, "Entering in the #42 add richDocuments templates");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_TEMPLATES + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 43 && newVersion >= 43) {
|
||||
Log_OC.i(TAG, "Entering in the #43 add ownerId and owner display name to file table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_OWNER_ID + " TEXT ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_OWNER_DISPLAY_NAME + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 44 && newVersion >= 44) {
|
||||
Log_OC.i(TAG, "Entering in the #44 add note to file table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_NOTE + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 45 && newVersion >= 45) {
|
||||
Log_OC.i(TAG, "Entering in the #45 add sharees to file table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_SHAREES + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 46 && newVersion >= 46) {
|
||||
Log_OC.i(TAG, "Entering in the #46 add optional mimetypes to capabilities table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_OPTIONAL_MIMETYPE_LIST
|
||||
+ " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 47 && newVersion >= 47) {
|
||||
Log_OC.i(TAG, "Entering in the #47 add askForPassword to capability table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD +
|
||||
" INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 48 && newVersion >= 48) {
|
||||
Log_OC.i(TAG, "Entering in the #48 add product name to capabilities table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 49 && newVersion >= 49) {
|
||||
Log_OC.i(TAG, "Entering in the #49 add extended support to capabilities table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_EXTENDED_SUPPORT + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 50 && newVersion >= 50) {
|
||||
Log_OC.i(TAG, "Entering in the #50 add persistent enable date to synced_folders table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS + " INTEGER ");
|
||||
|
||||
db.execSQL("UPDATE " + ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME + " SET " +
|
||||
ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS + " = CASE " +
|
||||
" WHEN enabled = 0 THEN " + SyncedFolder.EMPTY_ENABLED_TIMESTAMP_MS + " " +
|
||||
" ELSE " + clock.getCurrentTime() +
|
||||
" END ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 51 && newVersion >= 51) {
|
||||
Log_OC.i(TAG, "Entering in the #51 add show/hide to folderSync table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_HIDDEN + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 52 && newVersion >= 52) {
|
||||
Log_OC.i(TAG, "Entering in the #52 add etag for directEditing to capability");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 53 && newVersion >= 53) {
|
||||
Log_OC.i(TAG, "Entering in the #53 add rich workspace to file table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (!checkIfColumnExists(db, ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME,
|
||||
ProviderMeta.ProviderTableMeta.FILE_RICH_WORKSPACE)) {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_RICH_WORKSPACE + " TEXT ");
|
||||
}
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 54 && newVersion >= 54) {
|
||||
Log_OC.i(TAG, "Entering in the #54 add synced.existing," +
|
||||
" rename uploads.force_overwrite to uploads.name_collision_policy");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// Add synced.existing
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXISTING + " INTEGER "); // boolean
|
||||
|
||||
|
||||
// Rename uploads.force_overwrite to uploads.name_collision_policy
|
||||
String tmpTableName = ProviderMeta.ProviderTableMeta.UPLOADS_TABLE_NAME + "_old";
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.UPLOADS_TABLE_NAME + " RENAME TO " + tmpTableName);
|
||||
createUploadsTable(db);
|
||||
db.execSQL("INSERT INTO " + ProviderMeta.ProviderTableMeta.UPLOADS_TABLE_NAME + " (" +
|
||||
ProviderMeta.ProviderTableMeta._ID + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_LOCAL_PATH + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_REMOTE_PATH + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_ACCOUNT_NAME + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_FILE_SIZE + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_STATUS + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_UPLOAD_TIME + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_LAST_RESULT + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_IS_WIFI_ONLY + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_CREATED_BY + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN +
|
||||
") " +
|
||||
" SELECT " +
|
||||
ProviderMeta.ProviderTableMeta._ID + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_LOCAL_PATH + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_REMOTE_PATH + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_ACCOUNT_NAME + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_FILE_SIZE + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_STATUS + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_UPLOAD_TIME + ", " +
|
||||
"force_overwrite" + ", " + // See FileUploader.NameCollisionPolicy
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_LAST_RESULT + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_IS_WIFI_ONLY + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_CREATED_BY + ", " +
|
||||
ProviderMeta.ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN +
|
||||
" FROM " + tmpTableName);
|
||||
db.execSQL("DROP TABLE " + tmpTableName);
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 55 && newVersion >= 55) {
|
||||
Log_OC.i(TAG, "Entering in the #55 add synced.name_collision_policy.");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// Add synced.name_collision_policy
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_NAME_COLLISION_POLICY + " INTEGER "); // integer
|
||||
|
||||
// make sure all existing folders set to FileUploader.NameCollisionPolicy.ASK_USER.
|
||||
db.execSQL("UPDATE " + ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME + " SET " +
|
||||
ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_NAME_COLLISION_POLICY + " = " +
|
||||
NameCollisionPolicy.ASK_USER.serialize());
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 56 && newVersion >= 56) {
|
||||
Log_OC.i(TAG, "Entering in the #56 add decrypted remote path");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// Add synced.name_collision_policy
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_PATH_DECRYPTED + " TEXT "); // strin
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 57 && newVersion >= 57) {
|
||||
Log_OC.i(TAG, "Entering in the #57 add etag for capabilities");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_ETAG + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 58 && newVersion >= 58) {
|
||||
Log_OC.i(TAG, "Entering in the #58 add public link to share table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.OCSHARES_SHARE_LINK + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 59 && newVersion >= 59) {
|
||||
Log_OC.i(TAG, "Entering in the #59 add public label to share table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.OCSHARES_SHARE_LABEL + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 60 && newVersion >= 60) {
|
||||
Log_OC.i(TAG, "Entering in the #60 add user status to capability table");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_USER_STATUS + " INTEGER ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI + " INTEGER ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 61 && newVersion >= 61) {
|
||||
Log_OC.i(TAG, "Entering in the #61 reset eTag to force capability refresh");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL("UPDATE capabilities SET etag = '' WHERE 1=1");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 62 && newVersion >= 62) {
|
||||
Log_OC.i(TAG, "Entering in the #62 add logo to capability");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_SERVER_LOGO + " TEXT ");
|
||||
|
||||
// force refresh
|
||||
db.execSQL("UPDATE capabilities SET etag = '' WHERE 1=1");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 63 && newVersion >= 63) {
|
||||
Log_OC.i(TAG, "Adding file locking columns");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// locking capabilities
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME + ADD_COLUMN + ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION + " TEXT ");
|
||||
// force refresh
|
||||
db.execSQL("UPDATE capabilities SET etag = '' WHERE 1=1");
|
||||
// locking properties
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCKED + " INTEGER "); // boolean
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCK_TYPE + " INTEGER ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCK_OWNER + " TEXT ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCK_OWNER_DISPLAY_NAME + " TEXT ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCK_OWNER_EDITOR + " TEXT ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCK_TIMESTAMP + " INTEGER ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCK_TIMEOUT + " INTEGER ");
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_LOCK_TOKEN + " TEXT ");
|
||||
db.execSQL("UPDATE " + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME + " SET " + ProviderMeta.ProviderTableMeta.FILE_ETAG + " = '' WHERE 1=1");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 64 && newVersion >= 64) {
|
||||
Log_OC.i(TAG, "Entering in the #64 add metadata size to files");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderMeta.ProviderTableMeta.FILE_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderMeta.ProviderTableMeta.FILE_METADATA_SIZE + " TEXT ");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(TAG, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
}
|
||||
|
||||
private void createUploadsTable(SupportSQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + ProviderMeta.ProviderTableMeta.UPLOADS_TABLE_NAME + "("
|
||||
+ ProviderMeta.ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_LOCAL_PATH + TEXT
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_REMOTE_PATH + TEXT
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_ACCOUNT_NAME + TEXT
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_FILE_SIZE + " LONG, "
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_STATUS + INTEGER // UploadStatus
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + INTEGER // Upload LocalBehaviour
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_UPLOAD_TIME + INTEGER
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY + INTEGER // boolean
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + INTEGER // boolean
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + INTEGER
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_LAST_RESULT + INTEGER // Upload LastResult
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY + INTEGER // boolean
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_IS_WIFI_ONLY + INTEGER // boolean
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_CREATED_BY + INTEGER // Upload createdBy
|
||||
+ ProviderMeta.ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN + " TEXT );");
|
||||
|
||||
/* before:
|
||||
// PRIMARY KEY should always imply NOT NULL. Unfortunately, due to a
|
||||
// bug in some early versions, this is not the case in SQLite.
|
||||
//db.execSQL("CREATE TABLE " + TABLE_UPLOAD + " (" + " path TEXT PRIMARY KEY NOT NULL UNIQUE,"
|
||||
// + " uploadStatus INTEGER NOT NULL, uploadObject TEXT NOT NULL);");
|
||||
// uploadStatus is used to easy filtering, it has precedence over
|
||||
// uploadObject.getUploadStatus()
|
||||
*/
|
||||
}
|
||||
|
||||
private boolean checkIfColumnExists(SupportSQLiteDatabase database, String table, String column) {
|
||||
Cursor cursor = database.query("SELECT * FROM " + table + " LIMIT 0");
|
||||
boolean exists = cursor.getColumnIndex(column) != -1;
|
||||
cursor.close();
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil.KEYWORD_NOT_NULL
|
||||
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil.TYPE_INTEGER
|
||||
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil.TYPE_TEXT
|
||||
|
||||
/**
|
||||
* Migration from version 67 to 68.
|
||||
*
|
||||
* This migration makes the local_id column NOT NULL, with -1 as a default value.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
class Migration67to68 : Migration(67, 68) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
val tableName = "filelist"
|
||||
val newTableTempName = "${tableName}_new"
|
||||
val newColumns = mapOf(
|
||||
"_id" to DatabaseMigrationUtil.TYPE_INTEGER_PRIMARY_KEY,
|
||||
"filename" to TYPE_TEXT,
|
||||
"encrypted_filename" to TYPE_TEXT,
|
||||
"path" to TYPE_TEXT,
|
||||
"path_decrypted" to TYPE_TEXT,
|
||||
"parent" to TYPE_INTEGER,
|
||||
"created" to TYPE_INTEGER,
|
||||
"modified" to TYPE_INTEGER,
|
||||
"content_type" to TYPE_TEXT,
|
||||
"content_length" to TYPE_INTEGER,
|
||||
"media_path" to TYPE_TEXT,
|
||||
"file_owner" to TYPE_TEXT,
|
||||
"last_sync_date" to TYPE_INTEGER,
|
||||
"last_sync_date_for_data" to TYPE_INTEGER,
|
||||
"modified_at_last_sync_for_data" to TYPE_INTEGER,
|
||||
"etag" to TYPE_TEXT,
|
||||
"etag_on_server" to TYPE_TEXT,
|
||||
"share_by_link" to TYPE_INTEGER,
|
||||
"permissions" to TYPE_TEXT,
|
||||
"remote_id" to TYPE_TEXT,
|
||||
"local_id" to "$TYPE_INTEGER $KEYWORD_NOT_NULL DEFAULT -1",
|
||||
"update_thumbnail" to TYPE_INTEGER,
|
||||
"is_downloading" to TYPE_INTEGER,
|
||||
"favorite" to TYPE_INTEGER,
|
||||
"is_encrypted" to TYPE_INTEGER,
|
||||
"etag_in_conflict" to TYPE_TEXT,
|
||||
"shared_via_users" to TYPE_INTEGER,
|
||||
"mount_type" to TYPE_INTEGER,
|
||||
"has_preview" to TYPE_INTEGER,
|
||||
"unread_comments_count" to TYPE_INTEGER,
|
||||
"owner_id" to TYPE_TEXT,
|
||||
"owner_display_name" to TYPE_TEXT,
|
||||
"note" to TYPE_TEXT,
|
||||
"sharees" to TYPE_TEXT,
|
||||
"rich_workspace" to TYPE_TEXT,
|
||||
"metadata_size" to TYPE_TEXT,
|
||||
"locked" to TYPE_INTEGER,
|
||||
"lock_type" to TYPE_INTEGER,
|
||||
"lock_owner" to TYPE_TEXT,
|
||||
"lock_owner_display_name" to TYPE_TEXT,
|
||||
"lock_owner_editor" to TYPE_TEXT,
|
||||
"lock_timestamp" to TYPE_INTEGER,
|
||||
"lock_timeout" to TYPE_INTEGER,
|
||||
"lock_token" to TYPE_TEXT
|
||||
)
|
||||
|
||||
DatabaseMigrationUtil.migrateTable(database, "filelist", newColumns) { columnName ->
|
||||
when (columnName) {
|
||||
"local_id" -> "IFNULL(local_id, -1)"
|
||||
else -> columnName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.nextcloud.client.database.NextcloudDatabase
|
||||
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil.TYPE_INTEGER
|
||||
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil.TYPE_INTEGER_PRIMARY_KEY
|
||||
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil.TYPE_TEXT
|
||||
|
||||
class RoomMigration : Migration(NextcloudDatabase.FIRST_ROOM_DB_VERSION - 1, NextcloudDatabase.FIRST_ROOM_DB_VERSION) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
migrateFilesystemTable(database)
|
||||
migrateUploadsTable(database)
|
||||
migrateCapabilitiesTable(database)
|
||||
migrateFilesTable(database)
|
||||
}
|
||||
|
||||
/**
|
||||
* filesystem table: STRING converted to TEXT
|
||||
*/
|
||||
private fun migrateFilesystemTable(database: SupportSQLiteDatabase) {
|
||||
val newColumns = mapOf(
|
||||
"_id" to TYPE_INTEGER_PRIMARY_KEY,
|
||||
"local_path" to TYPE_TEXT,
|
||||
"is_folder" to TYPE_INTEGER,
|
||||
"found_at" to TYPE_INTEGER,
|
||||
"upload_triggered" to TYPE_INTEGER,
|
||||
"syncedfolder_id" to TYPE_TEXT,
|
||||
"crc32" to TYPE_TEXT,
|
||||
"modified_at" to TYPE_INTEGER
|
||||
)
|
||||
|
||||
DatabaseMigrationUtil.migrateTable(database, "filesystem", newColumns)
|
||||
}
|
||||
|
||||
/**
|
||||
* uploads table: LONG converted to INTEGER
|
||||
*/
|
||||
private fun migrateUploadsTable(database: SupportSQLiteDatabase) {
|
||||
val newColumns = mapOf(
|
||||
"_id" to TYPE_INTEGER_PRIMARY_KEY,
|
||||
"local_path" to TYPE_TEXT,
|
||||
"remote_path" to TYPE_TEXT,
|
||||
"account_name" to TYPE_TEXT,
|
||||
"file_size" to TYPE_INTEGER,
|
||||
"status" to TYPE_INTEGER,
|
||||
"local_behaviour" to TYPE_INTEGER,
|
||||
"upload_time" to TYPE_INTEGER,
|
||||
"name_collision_policy" to TYPE_INTEGER,
|
||||
"is_create_remote_folder" to TYPE_INTEGER,
|
||||
"upload_end_timestamp" to TYPE_INTEGER,
|
||||
"last_result" to TYPE_INTEGER,
|
||||
"is_while_charging_only" to TYPE_INTEGER,
|
||||
"is_wifi_only" to TYPE_INTEGER,
|
||||
"created_by" to TYPE_INTEGER,
|
||||
"folder_unlock_token" to TYPE_TEXT
|
||||
)
|
||||
|
||||
DatabaseMigrationUtil.migrateTable(database, "list_of_uploads", newColumns)
|
||||
}
|
||||
|
||||
/**
|
||||
* capabilities table: "files_drop" column removed
|
||||
*/
|
||||
private fun migrateCapabilitiesTable(database: SupportSQLiteDatabase) {
|
||||
val newColumns = mapOf(
|
||||
"_id" to TYPE_INTEGER_PRIMARY_KEY,
|
||||
"account" to TYPE_TEXT,
|
||||
"version_mayor" to TYPE_INTEGER,
|
||||
"version_minor" to TYPE_INTEGER,
|
||||
"version_micro" to TYPE_INTEGER,
|
||||
"version_string" to TYPE_TEXT,
|
||||
"version_edition" to TYPE_TEXT,
|
||||
"extended_support" to TYPE_INTEGER,
|
||||
"core_pollinterval" to TYPE_INTEGER,
|
||||
"sharing_api_enabled" to TYPE_INTEGER,
|
||||
"sharing_public_enabled" to TYPE_INTEGER,
|
||||
"sharing_public_password_enforced" to TYPE_INTEGER,
|
||||
"sharing_public_expire_date_enabled" to TYPE_INTEGER,
|
||||
"sharing_public_expire_date_days" to TYPE_INTEGER,
|
||||
"sharing_public_expire_date_enforced" to TYPE_INTEGER,
|
||||
"sharing_public_send_mail" to TYPE_INTEGER,
|
||||
"sharing_public_upload" to TYPE_INTEGER,
|
||||
"sharing_user_send_mail" to TYPE_INTEGER,
|
||||
"sharing_resharing" to TYPE_INTEGER,
|
||||
"sharing_federation_outgoing" to TYPE_INTEGER,
|
||||
"sharing_federation_incoming" to TYPE_INTEGER,
|
||||
"files_bigfilechunking" to TYPE_INTEGER,
|
||||
"files_undelete" to TYPE_INTEGER,
|
||||
"files_versioning" to TYPE_INTEGER,
|
||||
"external_links" to TYPE_INTEGER,
|
||||
"server_name" to TYPE_TEXT,
|
||||
"server_color" to TYPE_TEXT,
|
||||
"server_text_color" to TYPE_TEXT,
|
||||
"server_element_color" to TYPE_TEXT,
|
||||
"server_slogan" to TYPE_TEXT,
|
||||
"server_logo" to TYPE_TEXT,
|
||||
"background_url" to TYPE_TEXT,
|
||||
"end_to_end_encryption" to TYPE_INTEGER,
|
||||
"activity" to TYPE_INTEGER,
|
||||
"background_default" to TYPE_INTEGER,
|
||||
"background_plain" to TYPE_INTEGER,
|
||||
"richdocument" to TYPE_INTEGER,
|
||||
"richdocument_mimetype_list" to TYPE_TEXT,
|
||||
"richdocument_direct_editing" to TYPE_INTEGER,
|
||||
"richdocument_direct_templates" to TYPE_INTEGER,
|
||||
"richdocument_optional_mimetype_list" to TYPE_TEXT,
|
||||
"sharing_public_ask_for_optional_password" to TYPE_INTEGER,
|
||||
"richdocument_product_name" to TYPE_TEXT,
|
||||
"direct_editing_etag" to TYPE_TEXT,
|
||||
"user_status" to TYPE_INTEGER,
|
||||
"user_status_supports_emoji" to TYPE_INTEGER,
|
||||
"etag" to TYPE_TEXT,
|
||||
"files_locking_version" to TYPE_TEXT
|
||||
)
|
||||
|
||||
DatabaseMigrationUtil.migrateTable(database, "capabilities", newColumns)
|
||||
}
|
||||
|
||||
/**
|
||||
* files table: "public_link" column removed
|
||||
*/
|
||||
private fun migrateFilesTable(database: SupportSQLiteDatabase) {
|
||||
val newColumns = mapOf(
|
||||
"_id" to TYPE_INTEGER_PRIMARY_KEY,
|
||||
"filename" to TYPE_TEXT,
|
||||
"encrypted_filename" to TYPE_TEXT,
|
||||
"path" to TYPE_TEXT,
|
||||
"path_decrypted" to TYPE_TEXT,
|
||||
"parent" to TYPE_INTEGER,
|
||||
"created" to TYPE_INTEGER,
|
||||
"modified" to TYPE_INTEGER,
|
||||
"content_type" to TYPE_TEXT,
|
||||
"content_length" to TYPE_INTEGER,
|
||||
"media_path" to TYPE_TEXT,
|
||||
"file_owner" to TYPE_TEXT,
|
||||
"last_sync_date" to TYPE_INTEGER,
|
||||
"last_sync_date_for_data" to TYPE_INTEGER,
|
||||
"modified_at_last_sync_for_data" to TYPE_INTEGER,
|
||||
"etag" to TYPE_TEXT,
|
||||
"etag_on_server" to TYPE_TEXT,
|
||||
"share_by_link" to TYPE_INTEGER,
|
||||
"permissions" to TYPE_TEXT,
|
||||
"remote_id" to TYPE_TEXT,
|
||||
"update_thumbnail" to TYPE_INTEGER,
|
||||
"is_downloading" to TYPE_INTEGER,
|
||||
"favorite" to TYPE_INTEGER,
|
||||
"is_encrypted" to TYPE_INTEGER,
|
||||
"etag_in_conflict" to TYPE_TEXT,
|
||||
"shared_via_users" to TYPE_INTEGER,
|
||||
"mount_type" to TYPE_INTEGER,
|
||||
"has_preview" to TYPE_INTEGER,
|
||||
"unread_comments_count" to TYPE_INTEGER,
|
||||
"owner_id" to TYPE_TEXT,
|
||||
"owner_display_name" to TYPE_TEXT,
|
||||
"note" to TYPE_TEXT,
|
||||
"sharees" to TYPE_TEXT,
|
||||
"rich_workspace" to TYPE_TEXT,
|
||||
"metadata_size" to TYPE_TEXT,
|
||||
"locked" to TYPE_INTEGER,
|
||||
"lock_type" to TYPE_INTEGER,
|
||||
"lock_owner" to TYPE_TEXT,
|
||||
"lock_owner_display_name" to TYPE_TEXT,
|
||||
"lock_owner_editor" to TYPE_TEXT,
|
||||
"lock_timestamp" to TYPE_INTEGER,
|
||||
"lock_timeout" to TYPE_INTEGER,
|
||||
"lock_token" to TYPE_TEXT
|
||||
)
|
||||
DatabaseMigrationUtil.migrateTable(database, "filelist", newColumns)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.device
|
||||
|
||||
/**
|
||||
* This class exposes battery status information
|
||||
* in platform-independent way.
|
||||
*
|
||||
* @param isCharging true if device battery is charging
|
||||
* @param level Battery level, from 0 to 100%
|
||||
*
|
||||
* @see [android.os.BatteryManager]
|
||||
*/
|
||||
data class BatteryStatus(val isCharging: Boolean = false, val level: Int = 0) {
|
||||
|
||||
companion object {
|
||||
const val BATTERY_FULL = 100
|
||||
}
|
||||
|
||||
/**
|
||||
* True if battery is fully loaded, false otherwise.
|
||||
* Some dodgy devices can report battery charging
|
||||
* status as "battery full".
|
||||
*/
|
||||
val isFull: Boolean get() = level >= BATTERY_FULL
|
||||
}
|
||||
22
app/src/main/java/com/nextcloud/client/device/DeviceInfo.kt
Normal file
22
app/src/main/java/com/nextcloud/client/device/DeviceInfo.kt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.device
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import java.util.Locale
|
||||
|
||||
class DeviceInfo {
|
||||
val vendor: String = Build.MANUFACTURER.toLowerCase(Locale.ROOT)
|
||||
val apiLevel: Int = Build.VERSION.SDK_INT
|
||||
val androidVersion = Build.VERSION.RELEASE
|
||||
|
||||
fun hasCamera(context: Context): Boolean {
|
||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.device
|
||||
|
||||
import android.content.Context
|
||||
import android.os.PowerManager
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@Module
|
||||
class DeviceModule {
|
||||
|
||||
@Provides
|
||||
fun powerManagementService(context: Context, preferences: AppPreferences): PowerManagementService {
|
||||
val platformPowerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return PowerManagementServiceImpl(
|
||||
context = context,
|
||||
platformPowerManager = platformPowerManager,
|
||||
deviceInfo = DeviceInfo(),
|
||||
preferences = preferences
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.device
|
||||
|
||||
/**
|
||||
* This service provides all device power management
|
||||
* functions.
|
||||
*/
|
||||
interface PowerManagementService {
|
||||
|
||||
/**
|
||||
* Checks if power saving mode is enabled on this device.
|
||||
* On platforms that do not support power saving mode it
|
||||
* evaluates to false.
|
||||
*
|
||||
* @see android.os.PowerManager.isPowerSaveMode
|
||||
*/
|
||||
val isPowerSavingEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Checks if the device vendor requires power saving
|
||||
* exclusion workaround.
|
||||
*
|
||||
* @return true if workaround is required, false otherwise
|
||||
*/
|
||||
val isPowerSavingExclusionAvailable: Boolean
|
||||
|
||||
/**
|
||||
* Checks current battery status using platform [android.os.BatteryManager]
|
||||
*/
|
||||
val battery: BatteryStatus
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.device
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.BatteryManager
|
||||
import android.os.PowerManager
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.nextcloud.client.preferences.AppPreferencesImpl
|
||||
import com.nextcloud.utils.extensions.registerBroadcastReceiver
|
||||
import com.owncloud.android.datamodel.ReceiverFlag
|
||||
|
||||
internal class PowerManagementServiceImpl(
|
||||
private val context: Context,
|
||||
private val platformPowerManager: PowerManager,
|
||||
private val preferences: AppPreferences,
|
||||
private val deviceInfo: DeviceInfo = DeviceInfo()
|
||||
) : PowerManagementService {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Vendors on this list use aggressive power saving methods that might
|
||||
* break application experience.
|
||||
*/
|
||||
val OVERLY_AGGRESSIVE_POWER_SAVING_VENDORS = setOf("samsung", "huawei", "xiaomi")
|
||||
|
||||
@JvmStatic
|
||||
fun fromContext(context: Context): PowerManagementServiceImpl {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val preferences = AppPreferencesImpl.fromContext(context)
|
||||
|
||||
return PowerManagementServiceImpl(context, powerManager, preferences, DeviceInfo())
|
||||
}
|
||||
}
|
||||
|
||||
override val isPowerSavingEnabled: Boolean
|
||||
get() {
|
||||
if (preferences.isPowerCheckDisabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
return platformPowerManager.isPowerSaveMode
|
||||
}
|
||||
|
||||
override val isPowerSavingExclusionAvailable: Boolean
|
||||
get() = deviceInfo.vendor in OVERLY_AGGRESSIVE_POWER_SAVING_VENDORS
|
||||
|
||||
@Suppress("MagicNumber") // 100% is 100, we're not doing Cobol
|
||||
override val battery: BatteryStatus
|
||||
get() {
|
||||
val intent: Intent? = context.registerBroadcastReceiver(
|
||||
null,
|
||||
IntentFilter(Intent.ACTION_BATTERY_CHANGED),
|
||||
ReceiverFlag.NotExported
|
||||
)
|
||||
val isCharging = intent?.let {
|
||||
when (it.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0)) {
|
||||
BatteryManager.BATTERY_PLUGGED_USB -> true
|
||||
BatteryManager.BATTERY_PLUGGED_AC -> true
|
||||
BatteryManager.BATTERY_PLUGGED_WIRELESS -> true
|
||||
else -> false
|
||||
}
|
||||
} ?: false
|
||||
val level = intent?.let { it ->
|
||||
val level: Int = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||
val scale: Int = it.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
||||
(level * 100 / scale.toFloat()).toInt()
|
||||
} ?: 0
|
||||
return BatteryStatus(isCharging, level)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import dagger.android.AndroidInjection
|
||||
|
||||
class ActivityInjector : ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (activity is Injectable) {
|
||||
AndroidInjection.inject(activity)
|
||||
}
|
||||
if (activity is FragmentActivity) {
|
||||
val fm = activity.supportFragmentManager
|
||||
fm.registerFragmentLifecycleCallbacks(FragmentInjector(), true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
// unused atm
|
||||
}
|
||||
}
|
||||
73
app/src/main/java/com/nextcloud/client/di/AppComponent.java
Normal file
73
app/src/main/java/com/nextcloud/client/di/AppComponent.java
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.di;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.nextcloud.appReview.InAppReviewModule;
|
||||
import com.nextcloud.client.appinfo.AppInfoModule;
|
||||
import com.nextcloud.client.database.DatabaseModule;
|
||||
import com.nextcloud.client.device.DeviceModule;
|
||||
import com.nextcloud.client.integrations.IntegrationsModule;
|
||||
import com.nextcloud.client.jobs.JobsModule;
|
||||
import com.nextcloud.client.jobs.download.FileDownloadHelper;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.network.NetworkModule;
|
||||
import com.nextcloud.client.onboarding.OnboardingModule;
|
||||
import com.nextcloud.client.preferences.PreferencesModule;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.media.MediaControlView;
|
||||
import com.owncloud.android.ui.ThemeableSwitchPreference;
|
||||
import com.owncloud.android.ui.whatsnew.ProgressIndicator;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Component;
|
||||
import dagger.android.support.AndroidSupportInjectionModule;
|
||||
|
||||
@Component(modules = {
|
||||
AndroidSupportInjectionModule.class,
|
||||
AppModule.class,
|
||||
PreferencesModule.class,
|
||||
AppInfoModule.class,
|
||||
NetworkModule.class,
|
||||
DeviceModule.class,
|
||||
OnboardingModule.class,
|
||||
ViewModelModule.class,
|
||||
JobsModule.class,
|
||||
IntegrationsModule.class,
|
||||
InAppReviewModule.class,
|
||||
ThemeModule.class,
|
||||
DatabaseModule.class,
|
||||
DispatcherModule.class,
|
||||
VariantModule.class
|
||||
})
|
||||
@Singleton
|
||||
public interface AppComponent {
|
||||
|
||||
void inject(MainApp app);
|
||||
|
||||
void inject(MediaControlView mediaControlView);
|
||||
|
||||
void inject(ThemeableSwitchPreference switchPreference);
|
||||
|
||||
void inject(FileUploadHelper fileUploadHelper);
|
||||
|
||||
void inject(FileDownloadHelper fileDownloadHelper);
|
||||
|
||||
void inject(ProgressIndicator progressIndicator);
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
@BindsInstance
|
||||
Builder application(Application application);
|
||||
|
||||
AppComponent build();
|
||||
}
|
||||
}
|
||||
258
app/src/main/java/com/nextcloud/client/di/AppModule.java
Normal file
258
app/src/main/java/com/nextcloud/client/di/AppModule.java
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.di;
|
||||
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Handler;
|
||||
|
||||
import com.nextcloud.client.account.CurrentAccountProvider;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.nextcloud.client.account.UserAccountManagerImpl;
|
||||
import com.nextcloud.client.appinfo.AppInfo;
|
||||
import com.nextcloud.client.core.AsyncRunner;
|
||||
import com.nextcloud.client.core.Clock;
|
||||
import com.nextcloud.client.core.ClockImpl;
|
||||
import com.nextcloud.client.core.ThreadPoolAsyncRunner;
|
||||
import com.nextcloud.client.database.dao.ArbitraryDataDao;
|
||||
import com.nextcloud.client.device.DeviceInfo;
|
||||
import com.nextcloud.client.logger.FileLogHandler;
|
||||
import com.nextcloud.client.logger.Logger;
|
||||
import com.nextcloud.client.logger.LoggerImpl;
|
||||
import com.nextcloud.client.logger.LogsRepository;
|
||||
import com.nextcloud.client.migrations.Migrations;
|
||||
import com.nextcloud.client.migrations.MigrationsDb;
|
||||
import com.nextcloud.client.migrations.MigrationsManager;
|
||||
import com.nextcloud.client.migrations.MigrationsManagerImpl;
|
||||
import com.nextcloud.client.network.ClientFactory;
|
||||
import com.nextcloud.client.notifications.AppNotificationManager;
|
||||
import com.nextcloud.client.notifications.AppNotificationManagerImpl;
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.nextcloud.client.utils.Throttler;
|
||||
import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
|
||||
import com.owncloud.android.authentication.PassCodeManager;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||
import com.owncloud.android.datamodel.SyncedFolderProvider;
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager;
|
||||
import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository;
|
||||
import com.owncloud.android.ui.activities.data.activities.ActivitiesServiceApi;
|
||||
import com.owncloud.android.ui.activities.data.activities.ActivitiesServiceApiImpl;
|
||||
import com.owncloud.android.ui.activities.data.activities.RemoteActivitiesRepository;
|
||||
import com.owncloud.android.ui.activities.data.files.FilesRepository;
|
||||
import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl;
|
||||
import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository;
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module(includes = {ComponentsModule.class, VariantComponentsModule.class, BuildTypeComponentsModule.class})
|
||||
class AppModule {
|
||||
|
||||
@Provides
|
||||
AccountManager accountManager(Application application) {
|
||||
return (AccountManager) application.getSystemService(Context.ACCOUNT_SERVICE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
Context context(Application application) {
|
||||
return application;
|
||||
}
|
||||
|
||||
@Provides
|
||||
PackageManager packageManager(Application application) {
|
||||
return application.getPackageManager();
|
||||
}
|
||||
|
||||
@Provides
|
||||
ContentResolver contentResolver(Context context) {
|
||||
return context.getContentResolver();
|
||||
}
|
||||
|
||||
@Provides
|
||||
Resources resources(Application application) {
|
||||
return application.getResources();
|
||||
}
|
||||
|
||||
@Provides
|
||||
UserAccountManager userAccountManager(
|
||||
Context context,
|
||||
AccountManager accountManager) {
|
||||
return new UserAccountManagerImpl(context, accountManager);
|
||||
}
|
||||
|
||||
@Provides
|
||||
ArbitraryDataProvider arbitraryDataProvider(ArbitraryDataDao dao) {
|
||||
return new ArbitraryDataProviderImpl(dao);
|
||||
}
|
||||
|
||||
@Provides
|
||||
SyncedFolderProvider syncedFolderProvider(ContentResolver contentResolver,
|
||||
AppPreferences appPreferences,
|
||||
Clock clock) {
|
||||
return new SyncedFolderProvider(contentResolver, appPreferences, clock);
|
||||
}
|
||||
|
||||
@Provides
|
||||
ActivitiesServiceApi activitiesServiceApi(UserAccountManager accountManager) {
|
||||
return new ActivitiesServiceApiImpl(accountManager);
|
||||
}
|
||||
|
||||
@Provides
|
||||
ActivitiesRepository activitiesRepository(ActivitiesServiceApi api) {
|
||||
return new RemoteActivitiesRepository(api);
|
||||
}
|
||||
|
||||
@Provides
|
||||
FilesRepository filesRepository(UserAccountManager accountManager, ClientFactory clientFactory) {
|
||||
return new RemoteFilesRepository(new FilesServiceApiImpl(accountManager, clientFactory));
|
||||
}
|
||||
|
||||
@Provides
|
||||
UploadsStorageManager uploadsStorageManager(CurrentAccountProvider currentAccountProvider,
|
||||
Context context) {
|
||||
return new UploadsStorageManager(currentAccountProvider, context.getContentResolver());
|
||||
}
|
||||
|
||||
@Provides
|
||||
FileDataStorageManager fileDataStorageManager(CurrentAccountProvider currentAccountProvider,
|
||||
Context context) {
|
||||
return new FileDataStorageManager(currentAccountProvider.getUser(), context.getContentResolver());
|
||||
}
|
||||
|
||||
@Provides
|
||||
CurrentAccountProvider currentAccountProvider(UserAccountManager accountManager) {
|
||||
return accountManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
DeviceInfo deviceInfo() {
|
||||
return new DeviceInfo();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Clock clock() {
|
||||
return new ClockImpl();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Logger logger(Context context, Clock clock) {
|
||||
File logDir = new File(context.getFilesDir(), "logs");
|
||||
FileLogHandler handler = new FileLogHandler(logDir, "log.txt", 1024 * 1024);
|
||||
LoggerImpl logger = new LoggerImpl(clock, handler, new Handler(), 1000);
|
||||
logger.start();
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
LogsRepository logsRepository(Logger logger) {
|
||||
return (LogsRepository) logger;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
AsyncRunner uiAsyncRunner() {
|
||||
Handler uiHandler = new Handler();
|
||||
return new ThreadPoolAsyncRunner(uiHandler, 4, "ui");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("io")
|
||||
AsyncRunner ioAsyncRunner() {
|
||||
Handler uiHandler = new Handler();
|
||||
return new ThreadPoolAsyncRunner(uiHandler, 8, "io");
|
||||
}
|
||||
|
||||
@Provides
|
||||
NotificationManager notificationManager(Context context) {
|
||||
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
AudioManager audioManager(Context context) {
|
||||
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
EventBus eventBus() {
|
||||
return EventBus.getDefault();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MigrationsDb migrationsDb(Application application) {
|
||||
SharedPreferences store = application.getSharedPreferences("migrations", Context.MODE_PRIVATE);
|
||||
return new MigrationsDb(store);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MigrationsManager migrationsManager(MigrationsDb migrationsDb,
|
||||
AppInfo appInfo,
|
||||
AsyncRunner asyncRunner,
|
||||
Migrations migrations) {
|
||||
return new MigrationsManagerImpl(appInfo, migrationsDb, asyncRunner, migrations.getSteps());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
AppNotificationManager notificationsManager(Context context,
|
||||
NotificationManager platformNotificationsManager,
|
||||
Provider<ViewThemeUtils> viewThemeUtilsProvider) {
|
||||
return new AppNotificationManagerImpl(context,
|
||||
context.getResources(),
|
||||
platformNotificationsManager,
|
||||
viewThemeUtilsProvider.get());
|
||||
}
|
||||
|
||||
@Provides
|
||||
LocalBroadcastManager localBroadcastManager(Context context) {
|
||||
return LocalBroadcastManager.getInstance(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
Throttler throttler(Clock clock) {
|
||||
return new Throttler(clock);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
PassCodeManager passCodeManager(AppPreferences preferences, Clock clock) {
|
||||
return new PassCodeManager(preferences, clock);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
UsersAndGroupsSearchConfig userAndGroupSearchConfig() {
|
||||
return new UsersAndGroupsSearchConfig();
|
||||
}
|
||||
|
||||
}
|
||||
479
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
Normal file
479
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di;
|
||||
|
||||
import com.nextcloud.client.documentscan.DocumentScanActivity;
|
||||
import com.nextcloud.client.editimage.EditImageActivity;
|
||||
import com.nextcloud.client.etm.EtmActivity;
|
||||
import com.nextcloud.client.etm.pages.EtmBackgroundJobsFragment;
|
||||
import com.nextcloud.client.jobs.BackgroundJobManagerImpl;
|
||||
import com.nextcloud.client.jobs.NotificationWork;
|
||||
import com.nextcloud.client.jobs.TestJob;
|
||||
import com.nextcloud.client.jobs.transfer.FileTransferService;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.logger.ui.LogsActivity;
|
||||
import com.nextcloud.client.logger.ui.LogsViewModel;
|
||||
import com.nextcloud.client.media.PlayerService;
|
||||
import com.nextcloud.client.migrations.Migrations;
|
||||
import com.nextcloud.client.onboarding.FirstRunActivity;
|
||||
import com.nextcloud.client.onboarding.WhatsNewActivity;
|
||||
import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
|
||||
import com.nextcloud.client.widget.DashboardWidgetProvider;
|
||||
import com.nextcloud.client.widget.DashboardWidgetService;
|
||||
import com.nextcloud.ui.ChooseAccountDialogFragment;
|
||||
import com.nextcloud.ui.ImageDetailFragment;
|
||||
import com.nextcloud.ui.SetStatusDialogFragment;
|
||||
import com.nextcloud.ui.composeActivity.ComposeActivity;
|
||||
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
|
||||
import com.nmc.android.ui.LauncherActivity;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.authentication.AuthenticatorActivity;
|
||||
import com.owncloud.android.authentication.DeepLinkLoginActivity;
|
||||
import com.owncloud.android.files.BootupBroadcastReceiver;
|
||||
import com.owncloud.android.providers.DiskLruImageCacheFileProvider;
|
||||
import com.owncloud.android.providers.DocumentsStorageProvider;
|
||||
import com.owncloud.android.providers.FileContentProvider;
|
||||
import com.owncloud.android.providers.UsersAndGroupsSearchProvider;
|
||||
import com.owncloud.android.services.AccountManagerService;
|
||||
import com.owncloud.android.services.OperationsService;
|
||||
import com.owncloud.android.syncadapter.FileSyncService;
|
||||
import com.owncloud.android.ui.activities.ActivitiesActivity;
|
||||
import com.owncloud.android.ui.activity.BaseActivity;
|
||||
import com.owncloud.android.ui.activity.CommunityActivity;
|
||||
import com.owncloud.android.ui.activity.ConflictsResolveActivity;
|
||||
import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
|
||||
import com.owncloud.android.ui.activity.CopyToClipboardActivity;
|
||||
import com.owncloud.android.ui.activity.DrawerActivity;
|
||||
import com.owncloud.android.ui.activity.ErrorsWhileCopyingHandlerActivity;
|
||||
import com.owncloud.android.ui.activity.ExternalSiteWebView;
|
||||
import com.owncloud.android.ui.activity.FileActivity;
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity;
|
||||
import com.owncloud.android.ui.activity.FilePickerActivity;
|
||||
import com.owncloud.android.ui.activity.FolderPickerActivity;
|
||||
import com.owncloud.android.ui.activity.ManageAccountsActivity;
|
||||
import com.owncloud.android.ui.activity.ManageSpaceActivity;
|
||||
import com.owncloud.android.ui.activity.NotificationsActivity;
|
||||
import com.owncloud.android.ui.activity.PassCodeActivity;
|
||||
import com.owncloud.android.ui.activity.ReceiveExternalFilesActivity;
|
||||
import com.owncloud.android.ui.activity.RequestCredentialsActivity;
|
||||
import com.owncloud.android.ui.activity.RichDocumentsEditorWebView;
|
||||
import com.owncloud.android.ui.activity.SettingsActivity;
|
||||
import com.owncloud.android.ui.activity.ShareActivity;
|
||||
import com.owncloud.android.ui.activity.SsoGrantPermissionActivity;
|
||||
import com.owncloud.android.ui.activity.SyncedFoldersActivity;
|
||||
import com.owncloud.android.ui.activity.TextEditorWebView;
|
||||
import com.owncloud.android.ui.activity.ToolbarActivity;
|
||||
import com.owncloud.android.ui.activity.UploadFilesActivity;
|
||||
import com.owncloud.android.ui.activity.UploadListActivity;
|
||||
import com.owncloud.android.ui.activity.UserInfoActivity;
|
||||
import com.owncloud.android.ui.dialog.AccountRemovalDialog;
|
||||
import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
|
||||
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.IndeterminateProgressDialog;
|
||||
import com.owncloud.android.ui.dialog.LoadingDialog;
|
||||
import com.owncloud.android.ui.dialog.LocalStoragePathPickerDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.MultipleAccountsDialog;
|
||||
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.RenamePublicShareDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.SendFilesDialog;
|
||||
import com.owncloud.android.ui.dialog.SendShareDialog;
|
||||
import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.SharePasswordDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.SslUntrustedCertDialog;
|
||||
import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment;
|
||||
import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
|
||||
import com.owncloud.android.ui.fragment.ExtendedListFragment;
|
||||
import com.owncloud.android.ui.fragment.FeatureFragment;
|
||||
import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment;
|
||||
import com.owncloud.android.ui.fragment.FileDetailFragment;
|
||||
import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
|
||||
import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment;
|
||||
import com.owncloud.android.ui.fragment.GalleryFragment;
|
||||
import com.owncloud.android.ui.fragment.GalleryFragmentBottomSheetDialog;
|
||||
import com.owncloud.android.ui.fragment.GroupfolderListFragment;
|
||||
import com.owncloud.android.ui.fragment.LocalFileListFragment;
|
||||
import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog;
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment;
|
||||
import com.owncloud.android.ui.fragment.SharedListFragment;
|
||||
import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
|
||||
import com.owncloud.android.ui.preview.FileDownloadFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewBitmapActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewImageActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewImageFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFileFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewTextStringFragment;
|
||||
import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment;
|
||||
import com.owncloud.android.ui.trashbin.TrashbinActivity;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.android.ContributesAndroidInjector;
|
||||
|
||||
/**
|
||||
* Register classes that require dependency injection. This class is used by Dagger compiler only.
|
||||
*/
|
||||
@Module
|
||||
abstract class ComponentsModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract ActivitiesActivity activitiesActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract AuthenticatorActivity authenticatorActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract BaseActivity baseActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ConflictsResolveActivity conflictsResolveActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ContactsPreferenceActivity contactsPreferenceActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CopyToClipboardActivity copyToClipboardActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DeepLinkLoginActivity deepLinkLoginActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DrawerActivity drawerActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ErrorsWhileCopyingHandlerActivity errorsWhileCopyingHandlerActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ExternalSiteWebView externalSiteWebView();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileDisplayActivity fileDisplayActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FilePickerActivity filePickerActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FirstRunActivity firstRunActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FolderPickerActivity folderPickerActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LogsActivity logsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ManageAccountsActivity manageAccountsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ManageSpaceActivity manageSpaceActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract NotificationsActivity notificationsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CommunityActivity participateActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ComposeActivity composeActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PassCodeActivity passCodeActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewImageActivity previewImageActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewMediaActivity previewMediaActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ReceiveExternalFilesActivity receiveExternalFilesActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract RequestCredentialsActivity requestCredentialsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SettingsActivity settingsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ShareActivity shareActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SsoGrantPermissionActivity ssoGrantPermissionActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SyncedFoldersActivity syncedFoldersActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract TrashbinActivity trashbinActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UploadFilesActivity uploadFilesActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UploadListActivity uploadListActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UserInfoActivity userInfoActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract WhatsNewActivity whatsNewActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract EtmActivity etmActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract RichDocumentsEditorWebView richDocumentsWebView();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract TextEditorWebView textEditorWebView();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ExtendedListFragment extendedListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileDetailFragment fileDetailFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LocalFileListFragment localFileListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract OCFileListFragment ocFileListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileDetailActivitiesFragment fileDetailActivitiesFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileDetailsSharingProcessFragment fileDetailsSharingProcessFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileDetailSharingFragment fileDetailSharingFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ChooseTemplateDialogFragment chooseTemplateDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract AccountRemovalDialog accountRemovalDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ChooseRichDocumentsTemplateDialogFragment chooseRichDocumentsTemplateDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract BackupFragment contactsBackupFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewImageFragment previewImageFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract BackupListFragment chooseContactListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewMediaFragment previewMediaFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewTextFragment previewTextFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ChooseAccountDialogFragment chooseAccountDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SetStatusDialogFragment setStatusDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewTextFileFragment previewTextFileFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewTextStringFragment previewTextStringFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UnifiedSearchFragment searchFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract GalleryFragment photoFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract MultipleAccountsDialog multipleAccountsDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ReceiveExternalFilesActivity.DialogInputUploadFilename dialogInputUploadFilename();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract BootupBroadcastReceiver bootupBroadcastReceiver();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract NotificationWork.NotificationReceiver notificationWorkBroadcastReceiver();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileContentProvider fileContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UsersAndGroupsSearchProvider usersAndGroupsSearchProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DiskLruImageCacheFileProvider diskLruImageCacheFileProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DocumentsStorageProvider documentsStorageProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract AccountManagerService accountManagerService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract OperationsService operationsService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PlayerService playerService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileTransferService fileDownloaderService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileSyncService fileSyncService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DashboardWidgetService dashboardWidgetService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewPdfFragment previewPDFFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SharedListFragment sharedFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FeatureFragment featureFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract IndeterminateProgressDialog indeterminateProgressDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SortingOrderDialogFragment sortingOrderDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ConfirmationDialogFragment confirmationDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ConflictsResolveDialog conflictsResolveDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CreateFolderDialogFragment createFolderDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ExpirationDatePickerDialogFragment expirationDatePickerDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileActivity fileActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileDownloadFragment fileDownloadFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LoadingDialog loadingDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LocalStoragePathPickerDialogFragment localStoragePathPickerDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LogsViewModel logsViewModel();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract MainApp mainApp();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract Migrations migrations();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract NotificationWork notificationWork();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract RemoveFilesDialogFragment removeFilesDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract RenamePublicShareDialogFragment renamePublicShareDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SendShareDialog sendShareDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SetupEncryptionDialogFragment setupEncryptionDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SharePasswordDialogFragment sharePasswordDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SyncedFolderPreferencesDialogFragment syncedFolderPreferencesDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ToolbarActivity toolbarActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract StoragePermissionDialogFragment storagePermissionDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract OCFileListBottomSheetDialog ocfileListBottomSheetDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract RenameFileDialogFragment renameFileDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SyncFileNotEnoughSpaceDialogFragment syncFileNotEnoughSpaceDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DashboardWidgetConfigurationActivity dashboardWidgetConfigurationActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DashboardWidgetProvider dashboardWidgetProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract GalleryFragmentBottomSheetDialog galleryFragmentBottomSheetDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewBitmapActivity previewBitmapActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileUploadHelper fileUploadHelper();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SslUntrustedCertDialog sslUntrustedCertDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract FileActionsBottomSheet fileActionsBottomSheet();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SendFilesDialog sendFilesDialog();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DocumentScanActivity documentScanActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract GroupfolderListFragment groupfolderListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LauncherActivity launcherActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract EditImageActivity editImageActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ImageDetailFragment imageDetailFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract EtmBackgroundJobsFragment etmBackgroundJobsFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract BackgroundJobManagerImpl backgroundJobManagerImpl();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract TestJob testJob();
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class DefaultDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class IoDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class MainDispatcher
|
||||
|
||||
@Module
|
||||
object DispatcherModule {
|
||||
@DefaultDispatcher
|
||||
@Provides
|
||||
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@IoDispatcher
|
||||
@Provides
|
||||
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@MainDispatcher
|
||||
@Provides
|
||||
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import dagger.android.support.AndroidSupportInjection
|
||||
|
||||
internal class FragmentInjector : FragmentManager.FragmentLifecycleCallbacks() {
|
||||
override fun onFragmentPreAttached(
|
||||
fragmentManager: FragmentManager,
|
||||
fragment: Fragment,
|
||||
context: Context
|
||||
) {
|
||||
super.onFragmentPreAttached(fragmentManager, fragment, context)
|
||||
if (fragment is Injectable) {
|
||||
try {
|
||||
AndroidSupportInjection.inject(fragment)
|
||||
} catch (directCause: IllegalArgumentException) {
|
||||
// this provides a cause description that is a bit more friendly for developers
|
||||
throw InjectorNotFoundException(fragment, directCause)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/com/nextcloud/client/di/Injectable.java
Normal file
21
app/src/main/java/com/nextcloud/client/di/Injectable.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.di;
|
||||
|
||||
/**
|
||||
* Marks object as injectable by {@link ActivityInjector} and {@link FragmentInjector}.
|
||||
* <p>
|
||||
* Any {@link android.app.Activity} or {@link androidx.fragment.app.Fragment} implementing
|
||||
* this interface will be automatically supplied with dependencies.
|
||||
* <p>
|
||||
* Activities are considered fully-initialized after call to {@link android.app.Activity#onCreate(Bundle)}
|
||||
* (this means after {@code super.onCreate(savedStateInstance)} returns).
|
||||
* <p>
|
||||
* Injectable Fragments are supplied with dependencies before {@link androidx.fragment.app.Fragment#onAttach(Context)}.
|
||||
*/
|
||||
public interface Injectable {}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.di;
|
||||
|
||||
class InjectorNotFoundException extends RuntimeException {
|
||||
private static final long serialVersionUID = 2026042918255104421L;
|
||||
|
||||
InjectorNotFoundException(Object object, Throwable cause) {
|
||||
super(
|
||||
String.format(
|
||||
"Injector not registered for %s. Have you added it to %s?",
|
||||
object.getClass().getName(),
|
||||
ComponentsModule.class.getName()
|
||||
),
|
||||
cause
|
||||
);
|
||||
}
|
||||
}
|
||||
45
app/src/main/java/com/nextcloud/client/di/ThemeModule.kt
Normal file
45
app/src/main/java/com/nextcloud/client/di/ThemeModule.kt
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di
|
||||
|
||||
import com.nextcloud.android.common.ui.theme.MaterialSchemes
|
||||
import com.owncloud.android.utils.theme.MaterialSchemesProvider
|
||||
import com.owncloud.android.utils.theme.MaterialSchemesProviderImpl
|
||||
import com.owncloud.android.utils.theme.ThemeColorUtils
|
||||
import com.owncloud.android.utils.theme.ThemeUtils
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
internal abstract class ThemeModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindMaterialSchemesProvider(provider: MaterialSchemesProviderImpl): MaterialSchemesProvider
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun themeColorUtils(): ThemeColorUtils {
|
||||
return ThemeColorUtils()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun themeUtils(): ThemeUtils {
|
||||
return ThemeUtils()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideMaterialSchemes(materialSchemesProvider: MaterialSchemesProvider): MaterialSchemes {
|
||||
return materialSchemesProvider.getMaterialSchemesForCurrentUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* This factory provide [ViewModel] instances initialized by Dagger 2 dependency injection system.
|
||||
*
|
||||
* Each [javax.inject.Provider] instance accesses Dagger machinery, which provide
|
||||
* fully-initialized [ViewModel] instance.
|
||||
*
|
||||
* @see ViewModelModule
|
||||
* @see ViewModelKey
|
||||
*/
|
||||
class ViewModelFactory @Inject constructor(
|
||||
private val viewModelProviders: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
var vmProvider: Provider<ViewModel>? = viewModelProviders[modelClass]
|
||||
|
||||
if (vmProvider == null) {
|
||||
for (entry in viewModelProviders.entries) {
|
||||
if (modelClass.isAssignableFrom(entry.key)) {
|
||||
vmProvider = entry.value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vmProvider == null) {
|
||||
throw IllegalArgumentException("${modelClass.simpleName} view model class is not supported")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return vmProvider.get() as T
|
||||
}
|
||||
}
|
||||
17
app/src/main/java/com/nextcloud/client/di/ViewModelKey.kt
Normal file
17
app/src/main/java/com/nextcloud/client/di/ViewModelKey.kt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.MapKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@MustBeDocumented
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MapKey
|
||||
annotation class ViewModelKey(val value: KClass<out ViewModel>)
|
||||
55
app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
Normal file
55
app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.nextcloud.client.documentscan.DocumentScanViewModel
|
||||
import com.nextcloud.client.etm.EtmViewModel
|
||||
import com.nextcloud.client.logger.ui.LogsViewModel
|
||||
import com.nextcloud.ui.fileactions.FileActionsViewModel
|
||||
import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel
|
||||
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
abstract class ViewModelModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(EtmViewModel::class)
|
||||
abstract fun etmViewModel(vm: EtmViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(LogsViewModel::class)
|
||||
abstract fun logsViewModel(vm: LogsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(UnifiedSearchViewModel::class)
|
||||
abstract fun unifiedSearchViewModel(vm: UnifiedSearchViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(PreviewPdfViewModel::class)
|
||||
abstract fun previewPDFViewModel(vm: PreviewPdfViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(FileActionsViewModel::class)
|
||||
abstract fun fileActionsViewModel(vm: FileActionsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(DocumentScanViewModel::class)
|
||||
abstract fun documentScanViewModel(vm: DocumentScanViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
|
||||
}
|
||||
22
app/src/main/java/com/nextcloud/client/di/package-info.java
Normal file
22
app/src/main/java/com/nextcloud/client/di/package-info.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/**
|
||||
* This package contains application Dependency Injection code, based on Dagger 2.
|
||||
* <p>
|
||||
* To enable dependency injection for a component, such as {@link android.app.Activity},
|
||||
* {@link androidx.fragment.app.Fragment} or {@link android.app.Service}, the component must be
|
||||
* first registered in {@link com.nextcloud.client.di.ComponentsModule} class.
|
||||
* <p>
|
||||
* {@link com.nextcloud.client.di.ComponentsModule} will be used by Dagger compiler to
|
||||
* create an injector for a given class.
|
||||
*
|
||||
* @see com.nextcloud.client.di.InjectorNotFoundException
|
||||
* @see dagger.android.AndroidInjection
|
||||
* @see dagger.android.support.AndroidSupportInjection
|
||||
*/
|
||||
package com.nextcloud.client.di;
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.documentscan
|
||||
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
abstract class AppScanOptionalFeature {
|
||||
/**
|
||||
* Check [isAvailable] before calling this method.
|
||||
*/
|
||||
abstract fun getScanContract(): ActivityResultContract<Unit, String?>
|
||||
open val isAvailable: Boolean = true
|
||||
|
||||
/**
|
||||
* Use this in variants where the feature is not available
|
||||
*/
|
||||
@Suppress("unused") // used only in some variants
|
||||
object Stub : AppScanOptionalFeature() {
|
||||
override fun getScanContract(): ActivityResultContract<Unit, String?> {
|
||||
throw UnsupportedOperationException("Document scan is not available")
|
||||
}
|
||||
|
||||
override val isAvailable = false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.documentscan
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import com.owncloud.android.databinding.DocumentPageItemBinding
|
||||
|
||||
class DocumentPageListAdapter :
|
||||
ListAdapter<String, DocumentPageListAdapter.DocumentPageViewHolder>(DiffItemCallback()) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DocumentPageViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = DocumentPageItemBinding.inflate(inflater, parent, false)
|
||||
return DocumentPageViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DocumentPageViewHolder, position: Int) {
|
||||
holder.bind(currentList[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return currentList.size
|
||||
}
|
||||
|
||||
class DocumentPageViewHolder(val binding: DocumentPageItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(imagePath: String) {
|
||||
binding.root.load(imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffItemCallback : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String) =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String) =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.documentscan
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.di.ViewModelFactory
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.ActivityDocumentScanBinding
|
||||
import com.owncloud.android.databinding.DialogScanExportTypeBinding
|
||||
import com.owncloud.android.ui.activity.ToolbarActivity
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
class DocumentScanActivity : ToolbarActivity(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var vmFactory: ViewModelFactory
|
||||
|
||||
@Inject
|
||||
lateinit var logger: Logger
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appScanOptionalFeature: AppScanOptionalFeature
|
||||
|
||||
lateinit var binding: ActivityDocumentScanBinding
|
||||
|
||||
lateinit var viewModel: DocumentScanViewModel
|
||||
|
||||
private var scanPage: ActivityResultLauncher<Unit>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
scanPage = registerForActivityResult(appScanOptionalFeature.getScanContract()) { result ->
|
||||
viewModel.onScanPageResult(result)
|
||||
}
|
||||
|
||||
val folder = intent.extras?.getString(EXTRA_FOLDER)
|
||||
require(folder != null) { "Folder must be provided for upload" }
|
||||
|
||||
viewModel = ViewModelProvider(this, vmFactory)[DocumentScanViewModel::class.java]
|
||||
viewModel.setUploadFolder(folder)
|
||||
|
||||
setupViews()
|
||||
|
||||
observeState()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
binding = ActivityDocumentScanBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setupToolbar()
|
||||
supportActionBar?.let {
|
||||
it.setDisplayHomeAsUpEnabled(true)
|
||||
it.setDisplayShowHomeEnabled(true)
|
||||
viewThemeUtils.files.themeActionBar(this, it)
|
||||
}
|
||||
|
||||
viewThemeUtils.material.themeFAB(binding.fab)
|
||||
binding.fab.setOnClickListener {
|
||||
viewModel.onAddPageClicked()
|
||||
}
|
||||
|
||||
binding.pagesRecycler.layoutManager = GridLayoutManager(this, PAGE_COLUMNS)
|
||||
|
||||
setupMenu()
|
||||
}
|
||||
|
||||
private fun setupMenu() {
|
||||
addMenuProvider(
|
||||
object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.activity_document_scan, menu)
|
||||
menu.findItem(R.id.action_save)?.let {
|
||||
viewThemeUtils.platform.colorToolbarMenuIcon(this@DocumentScanActivity, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_save -> {
|
||||
viewModel.onClickDone()
|
||||
true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeState() {
|
||||
viewModel.uiState.observe(this, ::handleState)
|
||||
}
|
||||
|
||||
private fun handleState(state: DocumentScanViewModel.UIState) {
|
||||
logger.d(TAG, "handleState: called with $state")
|
||||
when (state) {
|
||||
is DocumentScanViewModel.UIState.BaseState -> when (state) {
|
||||
is DocumentScanViewModel.UIState.NormalState -> {
|
||||
updateButtonsEnabled(true)
|
||||
val pageList = state.pageList
|
||||
updateRecycler(pageList)
|
||||
if (state.shouldRequestScan) {
|
||||
startPageScan()
|
||||
}
|
||||
}
|
||||
is DocumentScanViewModel.UIState.RequestExportState -> {
|
||||
updateButtonsEnabled(false)
|
||||
if (state.shouldRequestExportType) {
|
||||
showExportDialog()
|
||||
viewModel.onRequestTypeHandled()
|
||||
}
|
||||
}
|
||||
}
|
||||
DocumentScanViewModel.UIState.DoneState, DocumentScanViewModel.UIState.CanceledState -> {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showExportDialog() {
|
||||
val dialogBinding = DialogScanExportTypeBinding.inflate(layoutInflater)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.document_scan_export_dialog_title)
|
||||
.setCancelable(true)
|
||||
.setView(dialogBinding.root)
|
||||
.setNegativeButton(R.string.common_cancel) { _, _ ->
|
||||
viewModel.onExportCanceled()
|
||||
}
|
||||
.setOnCancelListener { viewModel.onExportCanceled() }
|
||||
.also {
|
||||
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this@DocumentScanActivity, it)
|
||||
}
|
||||
.create()
|
||||
|
||||
viewThemeUtils.platform.colorTextButtons(dialogBinding.btnPdf, dialogBinding.btnImages)
|
||||
|
||||
dialogBinding.btnPdf.setOnClickListener {
|
||||
viewModel.onExportTypeSelected(DocumentScanViewModel.ExportType.PDF)
|
||||
dialog.dismiss()
|
||||
}
|
||||
dialogBinding.btnImages.setOnClickListener {
|
||||
viewModel.onExportTypeSelected(DocumentScanViewModel.ExportType.IMAGES)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val alertDialog = it as AlertDialog
|
||||
viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE))
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun updateRecycler(pageList: List<String>) {
|
||||
if (binding.pagesRecycler.adapter == null) {
|
||||
binding.pagesRecycler.adapter = DocumentPageListAdapter()
|
||||
}
|
||||
(binding.pagesRecycler.adapter as? DocumentPageListAdapter)?.submitList(pageList)
|
||||
}
|
||||
|
||||
private fun updateButtonsEnabled(enabled: Boolean) {
|
||||
binding.fab.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun startPageScan() {
|
||||
logger.d(TAG, "startPageScan() called")
|
||||
viewModel.onScanRequestHandled()
|
||||
scanPage!!.launch(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DocumentScanActivity"
|
||||
private const val PAGE_COLUMNS = 2
|
||||
const val EXTRA_FOLDER = "extra_folder"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.documentscan
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.nextcloud.client.account.CurrentAccountProvider
|
||||
import com.nextcloud.client.di.IoDispatcher
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.files.services.NameCollisionPolicy
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.ui.helpers.FileOperationsHelper
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("Detekt.LongParameterList") // satisfied by DI
|
||||
class DocumentScanViewModel @Inject constructor(
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
app: Application,
|
||||
private val logger: Logger,
|
||||
private val backgroundJobManager: BackgroundJobManager,
|
||||
private val currentAccountProvider: CurrentAccountProvider
|
||||
) : AndroidViewModel(app) {
|
||||
init {
|
||||
logger.d(TAG, "DocumentScanViewModel created")
|
||||
}
|
||||
|
||||
sealed interface UIState {
|
||||
sealed class BaseState(val pageList: List<String>) : UIState {
|
||||
val isEmpty: Boolean
|
||||
get() = pageList.isEmpty()
|
||||
}
|
||||
|
||||
class NormalState(
|
||||
pageList: List<String> = emptyList(),
|
||||
val shouldRequestScan: Boolean = false
|
||||
) : BaseState(pageList)
|
||||
|
||||
class RequestExportState(
|
||||
pageList: List<String> = emptyList(),
|
||||
val shouldRequestExportType: Boolean = true
|
||||
) : BaseState(pageList)
|
||||
|
||||
object DoneState : UIState
|
||||
object CanceledState : UIState
|
||||
}
|
||||
|
||||
private var uploadFolder: String? = null
|
||||
private val initialState = UIState.NormalState(shouldRequestScan = true)
|
||||
private val _uiState = MutableLiveData<UIState>(initialState)
|
||||
val uiState: LiveData<UIState>
|
||||
get() = _uiState
|
||||
|
||||
/**
|
||||
* @param result should be the path to the scanned page on the disk
|
||||
*/
|
||||
fun onScanPageResult(result: String?) {
|
||||
logger.d(TAG, "onScanPageResult() called with: result = $result")
|
||||
|
||||
val state = _uiState.value
|
||||
require(state is UIState.NormalState)
|
||||
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
if (result != null) {
|
||||
val newPath = renameCapturedImage(result)
|
||||
val pageList = state.pageList.toMutableList()
|
||||
pageList.add(newPath)
|
||||
_uiState.postValue(UIState.NormalState(pageList))
|
||||
} else {
|
||||
// result == null means cancellation or error
|
||||
if (state.isEmpty) {
|
||||
// close only if no pages have been added yet
|
||||
_uiState.postValue(UIState.CanceledState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO extract to usecase
|
||||
private fun renameCapturedImage(originalPath: String): String {
|
||||
val file = File(originalPath)
|
||||
val renamedFile =
|
||||
File(
|
||||
getApplication<Application>().cacheDir.path +
|
||||
File.separator + FileOperationsHelper.getCapturedImageName()
|
||||
)
|
||||
file.renameTo(renamedFile)
|
||||
return renamedFile.absolutePath
|
||||
}
|
||||
|
||||
fun onScanRequestHandled() {
|
||||
val state = uiState.value
|
||||
require(state is UIState.NormalState)
|
||||
|
||||
_uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = false))
|
||||
}
|
||||
|
||||
fun onAddPageClicked() {
|
||||
val state = uiState.value
|
||||
require(state is UIState.NormalState)
|
||||
if (!state.shouldRequestScan) {
|
||||
_uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = true))
|
||||
}
|
||||
}
|
||||
|
||||
fun onClickDone() {
|
||||
val state = _uiState.value
|
||||
if (state is UIState.BaseState && !state.isEmpty) {
|
||||
_uiState.postValue(UIState.RequestExportState(state.pageList))
|
||||
}
|
||||
}
|
||||
|
||||
fun setUploadFolder(folder: String) {
|
||||
this.uploadFolder = folder
|
||||
}
|
||||
|
||||
fun onRequestTypeHandled() {
|
||||
val state = _uiState.value
|
||||
require(state is UIState.RequestExportState)
|
||||
_uiState.postValue(UIState.RequestExportState(state.pageList, false))
|
||||
}
|
||||
|
||||
fun onExportTypeSelected(exportType: ExportType) {
|
||||
val state = _uiState.value
|
||||
require(state is UIState.RequestExportState)
|
||||
when (exportType) {
|
||||
ExportType.PDF -> {
|
||||
exportToPdf(state.pageList)
|
||||
}
|
||||
ExportType.IMAGES -> {
|
||||
exportToImages(state.pageList)
|
||||
}
|
||||
}
|
||||
_uiState.postValue(UIState.DoneState)
|
||||
}
|
||||
|
||||
private fun exportToPdf(pageList: List<String>) {
|
||||
val genPath =
|
||||
getApplication<Application>().cacheDir.path + File.separator + FileOperationsHelper.getTimestampedFileName(
|
||||
".pdf"
|
||||
)
|
||||
backgroundJobManager.startPdfGenerateAndUploadWork(
|
||||
currentAccountProvider.user,
|
||||
uploadFolder!!,
|
||||
pageList,
|
||||
genPath
|
||||
)
|
||||
// after job is started, finish the application.
|
||||
_uiState.postValue(UIState.DoneState)
|
||||
}
|
||||
|
||||
private fun exportToImages(pageList: List<String>) {
|
||||
val uploadPaths = pageList.map {
|
||||
uploadFolder + OCFile.PATH_SEPARATOR + File(it).name
|
||||
}.toTypedArray()
|
||||
|
||||
FileUploadHelper.instance().uploadNewFiles(
|
||||
currentAccountProvider.user,
|
||||
pageList.toTypedArray(),
|
||||
uploadPaths,
|
||||
FileUploadWorker.LOCAL_BEHAVIOUR_DELETE,
|
||||
true,
|
||||
UploadFileOperation.CREATED_BY_USER,
|
||||
false,
|
||||
false,
|
||||
NameCollisionPolicy.ASK_USER
|
||||
)
|
||||
}
|
||||
|
||||
fun onExportCanceled() {
|
||||
val state = _uiState.value
|
||||
if (state is UIState.BaseState) {
|
||||
_uiState.postValue(UIState.NormalState(state.pageList))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "DocumentScanViewModel"
|
||||
}
|
||||
|
||||
enum class ExportType {
|
||||
PDF,
|
||||
IMAGES
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.documentscan
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.pdf.PdfDocument
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This class takes a list of images and generates a PDF file.
|
||||
*/
|
||||
class GeneratePDFUseCase @Inject constructor(private val logger: Logger) {
|
||||
/**
|
||||
* @param imagePaths list of image paths
|
||||
* @return `true` if the PDF was generated successfully, `false` otherwise
|
||||
*/
|
||||
fun execute(imagePaths: List<String>, filePath: String): Boolean {
|
||||
return if (imagePaths.isEmpty() || filePath.isBlank()) {
|
||||
logger.w(TAG, "Invalid parameters: imagePaths: $imagePaths, filePath: $filePath")
|
||||
false
|
||||
} else {
|
||||
val document = PdfDocument()
|
||||
fillDocumentPages(document, imagePaths)
|
||||
writePdfToFile(filePath, document)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return `true` if the PDF was generated successfully, `false` otherwise
|
||||
*/
|
||||
private fun writePdfToFile(
|
||||
filePath: String,
|
||||
document: PdfDocument
|
||||
): Boolean {
|
||||
return try {
|
||||
val fileOutputStream = FileOutputStream(filePath)
|
||||
document.writeTo(fileOutputStream)
|
||||
fileOutputStream.close()
|
||||
document.close()
|
||||
true
|
||||
} catch (ex: IOException) {
|
||||
logger.e(TAG, "Error generating PDF", ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillDocumentPages(
|
||||
document: PdfDocument,
|
||||
imagePaths: List<String>
|
||||
) {
|
||||
imagePaths.forEach { path ->
|
||||
val bitmap = BitmapFactory.decodeFile(path)
|
||||
val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, 1).create()
|
||||
val page = document.startPage(pageInfo)
|
||||
page.canvas.drawBitmap(bitmap, 0f, 0f, null)
|
||||
document.finishPage(page)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GeneratePDFUseCase"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.documentscan
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.AnonymousUser
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.files.services.NameCollisionPolicy
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
@Suppress("Detekt.LongParameterList") // constructed only from factory method and tests
|
||||
class GeneratePdfFromImagesWork(
|
||||
private val appContext: Context,
|
||||
private val generatePdfUseCase: GeneratePDFUseCase,
|
||||
private val viewThemeUtils: ViewThemeUtils,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val userAccountManager: UserAccountManager,
|
||||
private val logger: Logger,
|
||||
params: WorkerParameters
|
||||
) : Worker(appContext, params) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
val inputPaths = inputData.getStringArray(INPUT_IMAGE_FILE_PATHS)?.toList()
|
||||
val outputFilePath = inputData.getString(INPUT_OUTPUT_FILE_PATH)
|
||||
val uploadFolder = inputData.getString(INPUT_UPLOAD_FOLDER)
|
||||
val accountName = inputData.getString(INPUT_UPLOAD_ACCOUNT)
|
||||
|
||||
@Suppress("Detekt.ComplexCondition") // not that complex
|
||||
require(!inputPaths.isNullOrEmpty() && outputFilePath != null && uploadFolder != null && accountName != null) {
|
||||
"PDF generation work started with missing parameters:" +
|
||||
" inputPaths: $inputPaths, outputFilePath: $outputFilePath," +
|
||||
" uploadFolder: $uploadFolder, accountName: $accountName"
|
||||
}
|
||||
|
||||
val user = userAccountManager.getUser(accountName)
|
||||
require(user.isPresent && user.get() !is AnonymousUser) { "Invalid or not found user" }
|
||||
|
||||
logger.d(
|
||||
TAG,
|
||||
"PDF generation work started with parameters: inputPaths=$inputPaths," +
|
||||
"outputFilePath=$outputFilePath, uploadFolder=$uploadFolder, accountName=$accountName"
|
||||
)
|
||||
|
||||
val notificationId = showNotification(R.string.document_scan_pdf_generation_in_progress)
|
||||
val result = generatePdfUseCase.execute(inputPaths, outputFilePath)
|
||||
notificationManager.cancel(notificationId)
|
||||
if (result) {
|
||||
uploadFile(user.get(), uploadFolder, outputFilePath)
|
||||
cleanupImages(inputPaths)
|
||||
} else {
|
||||
logger.w(TAG, "PDF generation failed")
|
||||
showNotification(R.string.document_scan_pdf_generation_failed)
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
logger.d(TAG, "PDF generation work finished")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun cleanupImages(inputPaths: List<String>) {
|
||||
inputPaths.forEach {
|
||||
val deleted = File(it).delete()
|
||||
logger.d(TAG, "Deleted $it: success = $deleted")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotification(@StringRes messageRes: Int): Int {
|
||||
val notificationId = SecureRandom().nextInt()
|
||||
val message = appContext.getString(messageRes)
|
||||
|
||||
val notificationBuilder = NotificationCompat.Builder(
|
||||
appContext,
|
||||
NotificationUtils.NOTIFICATION_CHANNEL_GENERAL
|
||||
)
|
||||
.setSmallIcon(R.drawable.notification_icon)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(appContext.resources, R.drawable.notification_icon))
|
||||
.setContentText(message)
|
||||
.setAutoCancel(true)
|
||||
|
||||
viewThemeUtils.androidx.themeNotificationCompatBuilder(appContext, notificationBuilder)
|
||||
|
||||
notificationManager.notify(notificationId, notificationBuilder.build())
|
||||
|
||||
return notificationId
|
||||
}
|
||||
|
||||
private fun uploadFile(user: User, uploadFolder: String, pdfPath: String) {
|
||||
val uploadPath = uploadFolder + OCFile.PATH_SEPARATOR + File(pdfPath).name
|
||||
|
||||
FileUploadHelper().uploadNewFiles(
|
||||
user,
|
||||
arrayOf(pdfPath),
|
||||
arrayOf(uploadPath),
|
||||
FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, // MIME type will be detected from file name
|
||||
true,
|
||||
UploadFileOperation.CREATED_BY_USER,
|
||||
false,
|
||||
false,
|
||||
NameCollisionPolicy.ASK_USER
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INPUT_IMAGE_FILE_PATHS = "input_image_file_paths"
|
||||
const val INPUT_OUTPUT_FILE_PATH = "input_output_file_path"
|
||||
const val INPUT_UPLOAD_FOLDER = "input_upload_folder"
|
||||
const val INPUT_UPLOAD_ACCOUNT = "input_upload_account"
|
||||
private const val TAG = "GeneratePdfFromImagesWo"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 ZetaTom
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.editimage
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.canhub.cropper.CropImageView
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.utils.extensions.getParcelableArgument
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.ActivityEditImageBinding
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.files.services.NameCollisionPolicy
|
||||
import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.ui.activity.FileActivity
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.MimeType
|
||||
import java.io.File
|
||||
|
||||
class EditImageActivity :
|
||||
FileActivity(),
|
||||
OnRemoteOperationListener,
|
||||
CropImageView.OnSetImageUriCompleteListener,
|
||||
CropImageView.OnCropImageCompleteListener,
|
||||
Injectable {
|
||||
|
||||
private lateinit var binding: ActivityEditImageBinding
|
||||
private lateinit var file: OCFile
|
||||
private lateinit var format: Bitmap.CompressFormat
|
||||
|
||||
companion object {
|
||||
const val EXTRA_FILE = "FILE"
|
||||
const val OPEN_IMAGE_EDITOR = "OPEN_IMAGE_EDITOR"
|
||||
|
||||
private val supportedMimeTypes = arrayOf(
|
||||
MimeType.PNG,
|
||||
MimeType.JPEG,
|
||||
MimeType.WEBP,
|
||||
MimeType.TIFF,
|
||||
MimeType.BMP,
|
||||
MimeType.HEIC
|
||||
)
|
||||
|
||||
fun canBePreviewed(file: OCFile): Boolean {
|
||||
return file.mimeType in supportedMimeTypes
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityEditImageBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
file = intent.extras?.getParcelableArgument(EXTRA_FILE, OCFile::class.java)
|
||||
?: throw IllegalArgumentException("Missing file argument")
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.apply {
|
||||
title = file.fileName
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
|
||||
|
||||
window.statusBarColor = ContextCompat.getColor(this, R.color.black)
|
||||
window.navigationBarColor = getColor(R.color.black)
|
||||
|
||||
setupCropper()
|
||||
}
|
||||
|
||||
override fun onCropImageComplete(view: CropImageView, result: CropImageView.CropResult) {
|
||||
if (!result.isSuccessful) {
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.image_editor_unable_to_edit_image))
|
||||
return
|
||||
}
|
||||
val resultUri = result.getUriFilePath(this, false)
|
||||
val newFileName = file.fileName.substring(0, file.fileName.lastIndexOf('.')) +
|
||||
" " + getString(R.string.image_editor_file_edited_suffix) +
|
||||
resultUri?.substring(resultUri.lastIndexOf('.'))
|
||||
|
||||
resultUri?.let {
|
||||
FileUploadHelper().uploadNewFiles(
|
||||
user = storageManager.user,
|
||||
localPaths = arrayOf(it),
|
||||
remotePaths = arrayOf(file.parentRemotePath + File.separator + newFileName),
|
||||
createRemoteFolder = false,
|
||||
createdBy = UploadFileOperation.CREATED_BY_USER,
|
||||
requiresWifi = false,
|
||||
requiresCharging = false,
|
||||
nameCollisionPolicy = NameCollisionPolicy.RENAME,
|
||||
localBehavior = FileUploadWorker.LOCAL_BEHAVIOUR_DELETE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSetImageUriComplete(view: CropImageView, uri: Uri, error: Exception?) {
|
||||
if (error != null) {
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.image_editor_unable_to_edit_image))
|
||||
return
|
||||
}
|
||||
view.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
// add save button to action bar
|
||||
menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
|
||||
val saveIcon = AppCompatResources.getDrawable(this, R.drawable.ic_check)?.also {
|
||||
DrawableCompat.setTint(it, resources.getColor(R.color.white, theme))
|
||||
}
|
||||
menu?.findItem(R.id.custom_menu_placeholder_item)?.apply {
|
||||
icon = saveIcon
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
contentDescription = getString(R.string.common_save)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.custom_menu_placeholder_item -> {
|
||||
binding.cropImageView.croppedImageAsync(format)
|
||||
finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up image cropper and image editor control strip.
|
||||
*/
|
||||
private fun setupCropper() {
|
||||
val cropper = binding.cropImageView
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
binding.rotateLeft.setOnClickListener {
|
||||
cropper.rotateImage(-90)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
binding.rotateRight.setOnClickListener {
|
||||
cropper.rotateImage(90)
|
||||
}
|
||||
|
||||
binding.flipVertical.setOnClickListener {
|
||||
cropper.flipImageVertically()
|
||||
}
|
||||
|
||||
binding.flipHorizontal.setOnClickListener {
|
||||
cropper.flipImageHorizontally()
|
||||
}
|
||||
|
||||
cropper.setOnSetImageUriCompleteListener(this)
|
||||
cropper.setOnCropImageCompleteListener(this)
|
||||
cropper.setImageUriAsync(file.storageUri)
|
||||
|
||||
// determine output file format
|
||||
format = when (file.mimeType) {
|
||||
MimeType.PNG -> Bitmap.CompressFormat.PNG
|
||||
MimeType.WEBP -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Bitmap.CompressFormat.WEBP_LOSSY
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Bitmap.CompressFormat.WEBP
|
||||
}
|
||||
}
|
||||
|
||||
else -> Bitmap.CompressFormat.JPEG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2019 Andy Scherzinger <info@andy-scherzinger>
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2014 Luke Owncloud <owncloud@ohrt.org>
|
||||
* SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.errorhandling
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.owncloud.android.BuildConfig
|
||||
import com.owncloud.android.R
|
||||
|
||||
class ExceptionHandler(
|
||||
private val context: Context,
|
||||
private val defaultExceptionHandler: Thread.UncaughtExceptionHandler
|
||||
) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
companion object {
|
||||
private const val LINE_SEPARATOR = "\n"
|
||||
private const val EXCEPTION_FORMAT_MAX_RECURSIVITY = 10
|
||||
}
|
||||
|
||||
override fun uncaughtException(thread: Thread, exception: Throwable) {
|
||||
@Suppress("TooGenericExceptionCaught") // this is exactly what we want here
|
||||
try {
|
||||
val errorReport = generateErrorReport(formatException(thread, exception))
|
||||
val intent = Intent(context, ShowErrorActivity::class.java)
|
||||
intent.putExtra(ShowErrorActivity.EXTRA_ERROR_TEXT, errorReport)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
// Pass exception to OS for graceful handling - OS will report it via ADB
|
||||
// and close all activities and services.
|
||||
defaultExceptionHandler.uncaughtException(thread, exception)
|
||||
} catch (fatalException: Exception) {
|
||||
// do not recurse into custom handler if exception is thrown during
|
||||
// exception handling. Pass this ultimate fatal exception to OS
|
||||
defaultExceptionHandler.uncaughtException(thread, fatalException)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatException(thread: Thread, exception: Throwable): String {
|
||||
fun formatExceptionRecursive(thread: Thread, exception: Throwable, count: Int = 0): String {
|
||||
if (count > EXCEPTION_FORMAT_MAX_RECURSIVITY) {
|
||||
return "Max number of recursive exception causes exceeded!"
|
||||
}
|
||||
// print exception
|
||||
val stringBuilder = StringBuilder()
|
||||
val stackTrace = exception.stackTrace
|
||||
stringBuilder.appendLine("Exception in thread \"${thread.name}\" $exception")
|
||||
// print available stacktrace
|
||||
for (element in stackTrace) {
|
||||
stringBuilder.appendLine(" at $element")
|
||||
}
|
||||
// print cause recursively
|
||||
exception.cause?.let {
|
||||
stringBuilder.append("Caused by: ")
|
||||
stringBuilder.append(formatExceptionRecursive(thread, it, count + 1))
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
return formatExceptionRecursive(thread, exception, 0)
|
||||
}
|
||||
|
||||
private fun generateErrorReport(stackTrace: String): String {
|
||||
val buildNumber = context.resources.getString(R.string.buildNumber)
|
||||
|
||||
val buildNumberString = when {
|
||||
buildNumber.isNotEmpty() -> " (build #$buildNumber)"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
return """
|
||||
|### Cause of error
|
||||
|```java
|
||||
${stackTrace.prependIndent("|")}
|
||||
|```
|
||||
|
|
||||
|### App information
|
||||
|* ID: `${BuildConfig.APPLICATION_ID}`
|
||||
|* Version: `${BuildConfig.VERSION_CODE}$buildNumberString`
|
||||
|* Build flavor: `${BuildConfig.FLAVOR}`
|
||||
|
|
||||
|### Device information
|
||||
|* Brand: `${Build.BRAND}`
|
||||
|* Device: `${Build.DEVICE}`
|
||||
|* Model: `${Build.MODEL}`
|
||||
|* Id: `${Build.ID}`
|
||||
|* Product: `${Build.PRODUCT}`
|
||||
|
|
||||
|### Firmware
|
||||
|* SDK: `${Build.VERSION.SDK_INT}`
|
||||
|* Release: `${Build.VERSION.RELEASE}`
|
||||
|* Incremental: `${Build.VERSION.INCREMENTAL}`
|
||||
""".trimMargin("|")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.errorhandling
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.ActivityShowErrorBinding
|
||||
import com.owncloud.android.utils.ClipboardUtil
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import java.net.URLEncoder
|
||||
|
||||
class ShowErrorActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityShowErrorBinding
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ERROR_TEXT = "error"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityShowErrorBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.textViewError.text = intent.getStringExtra(EXTRA_ERROR_TEXT)
|
||||
|
||||
setSupportActionBar(binding.toolbarInclude.toolbar)
|
||||
supportActionBar!!.title = createErrorTitle()
|
||||
|
||||
val snackbar = DisplayUtils.createSnackbar(
|
||||
binding.errorPageContainer,
|
||||
R.string.error_report_issue_text,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.setAction(R.string.error_report_issue_action) { reportIssue() }
|
||||
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private fun createErrorTitle() = String.format(getString(R.string.error_crash_title), getString(R.string.app_name))
|
||||
|
||||
private fun reportIssue() {
|
||||
ClipboardUtil.copyToClipboard(this, binding.textViewError.text.toString(), false)
|
||||
val issueLink = String.format(
|
||||
getString(R.string.report_issue_link),
|
||||
URLEncoder.encode(binding.textViewError.text.toString(), Charsets.UTF_8.name())
|
||||
)
|
||||
DisplayUtils.startLinkIntent(this, issueLink)
|
||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_show_error, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.error_share -> {
|
||||
onClickedShare()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickedShare() {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, createErrorTitle())
|
||||
intent.putExtra(Intent.EXTRA_TEXT, binding.textViewError.text)
|
||||
intent.type = "text/plain"
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
82
app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt
Normal file
82
app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.di.ViewModelFactory
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.ui.activity.ToolbarActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class EtmActivity : ToolbarActivity(), Injectable {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun launch(context: Context) {
|
||||
val etmIntent = Intent(context, EtmActivity::class.java)
|
||||
context.startActivity(etmIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
internal lateinit var vm: EtmViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_etm)
|
||||
setupToolbar()
|
||||
updateActionBarTitleAndHomeButtonByString(getString(R.string.etm_title))
|
||||
vm = ViewModelProvider(this, viewModelFactory).get(EtmViewModel::class.java)
|
||||
vm.currentPage.observe(
|
||||
this,
|
||||
Observer {
|
||||
onPageChanged(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
if (!vm.onBackPressed()) {
|
||||
finish()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
if (!vm.onBackPressed()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPageChanged(page: EtmMenuEntry?) {
|
||||
if (page != null) {
|
||||
val fragment = page.pageClass.java.getConstructor().newInstance()
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.etm_page_container, fragment)
|
||||
.commit()
|
||||
updateActionBarTitleAndHomeButtonByString("ETM - ${getString(page.titleRes)}")
|
||||
} else {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.etm_page_container, EtmMenuFragment())
|
||||
.commitNow()
|
||||
updateActionBarTitleAndHomeButtonByString(getString(R.string.etm_title))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
abstract class EtmBaseFragment : Fragment() {
|
||||
protected val vm: EtmViewModel get() {
|
||||
return (activity as EtmActivity).vm
|
||||
}
|
||||
}
|
||||
55
app/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt
Normal file
55
app/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.owncloud.android.R
|
||||
|
||||
class EtmMenuAdapter(
|
||||
context: Context,
|
||||
val onItemClicked: (Int) -> Unit
|
||||
) : RecyclerView.Adapter<EtmMenuAdapter.PageViewHolder>() {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
var pages: List<EtmMenuEntry> = listOf()
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class PageViewHolder(view: View, onClick: (Int) -> Unit) : RecyclerView.ViewHolder(view) {
|
||||
val primaryAction: ImageView = view.findViewById(R.id.primary_action)
|
||||
val text: TextView = view.findViewById(R.id.text)
|
||||
val secondaryAction: ImageView = view.findViewById(R.id.secondary_action)
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener { onClick(adapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
|
||||
val view = layoutInflater.inflate(R.layout.material_list_item_single_line, parent, false)
|
||||
return PageViewHolder(view, onItemClicked)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
|
||||
val page = pages[position]
|
||||
holder.primaryAction.setImageResource(page.iconRes)
|
||||
holder.text.setText(page.titleRes)
|
||||
holder.secondaryAction.setImageResource(0)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return pages.size
|
||||
}
|
||||
}
|
||||
12
app/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt
Normal file
12
app/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
data class EtmMenuEntry(val iconRes: Int, val titleRes: Int, val pageClass: KClass<out Fragment>)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.owncloud.android.R
|
||||
|
||||
class EtmMenuFragment : EtmBaseFragment() {
|
||||
|
||||
private lateinit var adapter: EtmMenuAdapter
|
||||
private lateinit var list: RecyclerView
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
adapter = EtmMenuAdapter(requireContext(), this::onClickedItem)
|
||||
adapter.pages = vm.pages
|
||||
val view = inflater.inflate(R.layout.fragment_etm_menu, container, false)
|
||||
list = view.findViewById(R.id.etm_menu_list)
|
||||
list.layoutManager = LinearLayoutManager(requireContext())
|
||||
list.adapter = adapter
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.setTitle(R.string.etm_title)
|
||||
}
|
||||
|
||||
private fun onClickedItem(position: Int) {
|
||||
vm.onPageSelected(position)
|
||||
}
|
||||
}
|
||||
180
app/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt
Normal file
180
app/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.etm.pages.EtmAccountsFragment
|
||||
import com.nextcloud.client.etm.pages.EtmBackgroundJobsFragment
|
||||
import com.nextcloud.client.etm.pages.EtmFileTransferFragment
|
||||
import com.nextcloud.client.etm.pages.EtmMigrations
|
||||
import com.nextcloud.client.etm.pages.EtmPreferencesFragment
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.nextcloud.client.jobs.JobInfo
|
||||
import com.nextcloud.client.jobs.transfer.TransferManagerConnection
|
||||
import com.nextcloud.client.migrations.MigrationInfo
|
||||
import com.nextcloud.client.migrations.MigrationsDb
|
||||
import com.nextcloud.client.migrations.MigrationsManager
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.common.accounts.AccountUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList") // Dependencies Injection
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class EtmViewModel @Inject constructor(
|
||||
private val context: Context,
|
||||
private val defaultPreferences: SharedPreferences,
|
||||
private val platformAccountManager: AccountManager,
|
||||
private val accountManager: UserAccountManager,
|
||||
private val resources: Resources,
|
||||
private val backgroundJobManager: BackgroundJobManager,
|
||||
private val migrationsManager: MigrationsManager,
|
||||
private val migrationsDb: MigrationsDb
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
val ACCOUNT_USER_DATA_KEYS = listOf(
|
||||
// AccountUtils.Constants.KEY_COOKIES, is disabled
|
||||
AccountUtils.Constants.KEY_DISPLAY_NAME,
|
||||
AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION,
|
||||
AccountUtils.Constants.KEY_OC_BASE_URL,
|
||||
AccountUtils.Constants.KEY_OC_VERSION,
|
||||
AccountUtils.Constants.KEY_USER_ID
|
||||
)
|
||||
|
||||
const val PAGE_SETTINGS = 0
|
||||
const val PAGE_ACCOUNTS = 1
|
||||
const val PAGE_JOBS = 2
|
||||
const val PAGE_MIGRATIONS = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* This data class holds all relevant account information that is
|
||||
* otherwise kept in separate collections.
|
||||
*/
|
||||
data class AccountData(val account: Account, val userData: Map<String, String?>)
|
||||
|
||||
val currentUser: User get() = accountManager.user
|
||||
val currentPage: LiveData<EtmMenuEntry?> = MutableLiveData()
|
||||
val pages: List<EtmMenuEntry> = listOf(
|
||||
EtmMenuEntry(
|
||||
iconRes = R.drawable.ic_settings,
|
||||
titleRes = R.string.etm_preferences,
|
||||
pageClass = EtmPreferencesFragment::class
|
||||
),
|
||||
EtmMenuEntry(
|
||||
iconRes = R.drawable.ic_user,
|
||||
titleRes = R.string.etm_accounts,
|
||||
pageClass = EtmAccountsFragment::class
|
||||
),
|
||||
EtmMenuEntry(
|
||||
iconRes = R.drawable.ic_clock,
|
||||
titleRes = R.string.etm_background_jobs,
|
||||
pageClass = EtmBackgroundJobsFragment::class
|
||||
),
|
||||
EtmMenuEntry(
|
||||
iconRes = R.drawable.ic_arrow_up,
|
||||
titleRes = R.string.etm_migrations,
|
||||
pageClass = EtmMigrations::class
|
||||
),
|
||||
EtmMenuEntry(
|
||||
iconRes = R.drawable.ic_cloud_download,
|
||||
titleRes = R.string.etm_transfer,
|
||||
pageClass = EtmFileTransferFragment::class
|
||||
)
|
||||
)
|
||||
val transferManagerConnection = TransferManagerConnection(context, accountManager.user)
|
||||
|
||||
val preferences: Map<String, String> get() {
|
||||
return defaultPreferences.all
|
||||
.map { it.key to "${it.value}" }
|
||||
.sortedBy { it.first }
|
||||
.toMap()
|
||||
}
|
||||
|
||||
val accounts: List<AccountData> get() {
|
||||
val accountType = resources.getString(R.string.account_type)
|
||||
return platformAccountManager.getAccountsByType(accountType).map { account ->
|
||||
val userData: Map<String, String?> = ACCOUNT_USER_DATA_KEYS.map { key ->
|
||||
key to platformAccountManager.getUserData(account, key)
|
||||
}.toMap()
|
||||
AccountData(account, userData)
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundJobs: LiveData<List<JobInfo>> get() {
|
||||
return backgroundJobManager.jobs
|
||||
}
|
||||
|
||||
val migrationsInfo: List<MigrationInfo> get() {
|
||||
return migrationsManager.info
|
||||
}
|
||||
|
||||
val migrationsStatus: MigrationsManager.Status get() {
|
||||
return migrationsManager.status.value ?: MigrationsManager.Status.UNKNOWN
|
||||
}
|
||||
|
||||
val lastMigratedVersion: Int get() {
|
||||
return migrationsDb.lastMigratedVersion
|
||||
}
|
||||
|
||||
init {
|
||||
(currentPage as MutableLiveData).apply {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun onPageSelected(index: Int) {
|
||||
if (index < pages.size) {
|
||||
currentPage as MutableLiveData
|
||||
currentPage.value = pages[index]
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
(currentPage as MutableLiveData)
|
||||
return if (currentPage.value != null) {
|
||||
currentPage.value = null
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneJobs() {
|
||||
backgroundJobManager.pruneJobs()
|
||||
}
|
||||
|
||||
fun cancelAllJobs() {
|
||||
backgroundJobManager.cancelAllJobs()
|
||||
}
|
||||
|
||||
fun startTestJob(periodic: Boolean) {
|
||||
if (periodic) {
|
||||
backgroundJobManager.scheduleTestJob()
|
||||
} else {
|
||||
backgroundJobManager.startImmediateTestJob()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelTestJob() {
|
||||
backgroundJobManager.cancelTestJob()
|
||||
}
|
||||
|
||||
fun clearMigrations() {
|
||||
migrationsDb.clearMigrations()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm.pages
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.nextcloud.client.etm.EtmBaseFragment
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.FragmentEtmAccountsBinding
|
||||
|
||||
class EtmAccountsFragment : EtmBaseFragment() {
|
||||
private var _binding: FragmentEtmAccountsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
_binding = FragmentEtmAccountsBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val builder = StringBuilder()
|
||||
vm.accounts.forEach {
|
||||
builder.append("Account: ${it.account.name}\n")
|
||||
it.userData.forEach {
|
||||
builder.append("\t${it.key}: ${it.value}\n")
|
||||
}
|
||||
}
|
||||
binding.etmAccountsText.text = builder.toString()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.fragment_etm_accounts, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.etm_accounts_share -> {
|
||||
onClickedShare()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickedShare() {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "Nextcloud accounts information")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, binding.etmAccountsText.text)
|
||||
intent.type = "text/plain"
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm.pages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.etm.EtmBaseFragment
|
||||
import com.nextcloud.client.jobs.BackgroundJobManagerImpl
|
||||
import com.nextcloud.client.jobs.JobInfo
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.owncloud.android.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var preferences: AppPreferences
|
||||
|
||||
class Adapter(private val inflater: LayoutInflater, private val preferences: AppPreferences) :
|
||||
RecyclerView.Adapter<Adapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val uuid = view.findViewById<TextView>(R.id.etm_background_job_uuid)
|
||||
val name = view.findViewById<TextView>(R.id.etm_background_job_name)
|
||||
val user = view.findViewById<TextView>(R.id.etm_background_job_user)
|
||||
val state = view.findViewById<TextView>(R.id.etm_background_job_state)
|
||||
val started = view.findViewById<TextView>(R.id.etm_background_job_started)
|
||||
val progress = view.findViewById<TextView>(R.id.etm_background_job_progress)
|
||||
private val progressRow = view.findViewById<View>(R.id.etm_background_job_progress_row)
|
||||
val executionCount = view.findViewById<TextView>(R.id.etm_background_execution_count)
|
||||
val executionLog = view.findViewById<TextView>(R.id.etm_background_execution_logs)
|
||||
private val executionLogRow = view.findViewById<View>(R.id.etm_background_execution_logs_row)
|
||||
val executionTimesRow = view.findViewById<View>(R.id.etm_background_execution_times_row)
|
||||
|
||||
var progressEnabled: Boolean = progressRow.visibility == View.VISIBLE
|
||||
get() {
|
||||
return progressRow.visibility == View.VISIBLE
|
||||
}
|
||||
set(value) {
|
||||
field = value
|
||||
progressRow.visibility = if (value) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
var logsEnabled: Boolean = executionLogRow.visibility == View.VISIBLE
|
||||
get() {
|
||||
return executionLogRow.visibility == View.VISIBLE
|
||||
}
|
||||
set(value) {
|
||||
field = value
|
||||
executionLogRow.visibility = if (value) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:MM:ssZ", Locale.getDefault())
|
||||
var backgroundJobs: List<JobInfo> = emptyList()
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = inflater.inflate(R.layout.etm_background_job_list_item, parent, false)
|
||||
val viewHolder = ViewHolder(view)
|
||||
viewHolder.logsEnabled = false
|
||||
viewHolder.executionTimesRow.visibility = View.GONE
|
||||
view.setOnClickListener {
|
||||
viewHolder.logsEnabled = !viewHolder.logsEnabled
|
||||
}
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return backgroundJobs.size
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(vh: ViewHolder, position: Int) {
|
||||
val info = backgroundJobs[position]
|
||||
vh.uuid.text = info.id.toString()
|
||||
vh.name.text = info.name
|
||||
vh.user.text = info.user
|
||||
vh.state.text = info.state
|
||||
vh.started.text = dateFormat.format(info.started)
|
||||
if (info.progress >= 0) {
|
||||
vh.progressEnabled = true
|
||||
vh.progress.text = info.progress.toString()
|
||||
} else {
|
||||
vh.progressEnabled = false
|
||||
}
|
||||
|
||||
val logs = preferences.readLogEntry()
|
||||
val logsForThisWorker =
|
||||
logs.filter { BackgroundJobManagerImpl.parseTag(it.workerClass)?.second == info.workerClass }
|
||||
if (logsForThisWorker.isNotEmpty()) {
|
||||
vh.executionTimesRow.visibility = View.VISIBLE
|
||||
vh.executionCount.text =
|
||||
"${logsForThisWorker.filter { it.started != null }.size} " +
|
||||
"(${logsForThisWorker.filter { it.finished != null }.size})"
|
||||
var logText = "Worker Logs\n\n" +
|
||||
"*** Does NOT differentiate between immediate or periodic kinds of Work! ***\n" +
|
||||
"*** Times run in 48h: Times started (Times finished) ***\n"
|
||||
logsForThisWorker.forEach {
|
||||
logText += "----------------------\n"
|
||||
logText += "Worker ${BackgroundJobManagerImpl.parseTag(it.workerClass)?.second}\n"
|
||||
logText += if (it.started == null) {
|
||||
"ENDED at\n${it.finished}\nWith result: ${it.result}\n"
|
||||
} else {
|
||||
"STARTED at\n${it.started}\n"
|
||||
}
|
||||
}
|
||||
vh.executionLog.text = logText
|
||||
} else {
|
||||
vh.executionLog.text = "Worker Logs\n\n" +
|
||||
"No Entries -> Maybe logging is not implemented for Worker or it has not run yet."
|
||||
vh.executionCount.text = "0"
|
||||
vh.executionTimesRow.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var list: RecyclerView
|
||||
private lateinit var adapter: Adapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_etm_background_jobs, container, false)
|
||||
adapter = Adapter(inflater, preferences)
|
||||
list = view.findViewById(R.id.etm_background_jobs_list)
|
||||
list.layoutManager = LinearLayoutManager(context)
|
||||
list.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
list.adapter = adapter
|
||||
vm.backgroundJobs.observe(viewLifecycleOwner, Observer { onBackgroundJobsUpdated(it) })
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.fragment_etm_background_jobs, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.etm_background_jobs_cancel -> {
|
||||
vm.cancelAllJobs()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.etm_background_jobs_prune -> {
|
||||
vm.pruneJobs()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.etm_background_jobs_start_test -> {
|
||||
vm.startTestJob(periodic = false)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.etm_background_jobs_schedule_test -> {
|
||||
vm.startTestJob(periodic = true)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.etm_background_jobs_cancel_test -> {
|
||||
vm.cancelTestJob()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBackgroundJobsUpdated(backgroundJobs: List<JobInfo>) {
|
||||
adapter.backgroundJobs = backgroundJobs
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.etm.pages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.client.etm.EtmBaseFragment
|
||||
import com.nextcloud.client.files.DownloadRequest
|
||||
import com.nextcloud.client.files.UploadRequest
|
||||
import com.nextcloud.client.jobs.transfer.Transfer
|
||||
import com.nextcloud.client.jobs.transfer.TransferManager
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.db.OCUpload
|
||||
|
||||
class EtmFileTransferFragment : EtmBaseFragment() {
|
||||
|
||||
companion object {
|
||||
private const val TEST_DOWNLOAD_DUMMY_PATH = "/test/dummy_file.txt"
|
||||
}
|
||||
|
||||
class Adapter(private val inflater: LayoutInflater) : RecyclerView.Adapter<Adapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val type = view.findViewById<TextView>(R.id.etm_transfer_type)
|
||||
val typeIcon = view.findViewById<ImageView>(R.id.etm_transfer_type_icon)
|
||||
val uuid = view.findViewById<TextView>(R.id.etm_transfer_uuid)
|
||||
val path = view.findViewById<TextView>(R.id.etm_transfer_remote_path)
|
||||
val user = view.findViewById<TextView>(R.id.etm_transfer_user)
|
||||
val state = view.findViewById<TextView>(R.id.etm_transfer_state)
|
||||
val progress = view.findViewById<TextView>(R.id.etm_transfer_progress)
|
||||
private val progressRow = view.findViewById<View>(R.id.etm_transfer_progress_row)
|
||||
|
||||
var progressEnabled: Boolean = progressRow.visibility == View.VISIBLE
|
||||
get() {
|
||||
return progressRow.visibility == View.VISIBLE
|
||||
}
|
||||
set(value) {
|
||||
field = value
|
||||
progressRow.visibility = if (value) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var transfers = listOf<Transfer>()
|
||||
|
||||
fun setStatus(status: TransferManager.Status) {
|
||||
transfers = listOf(status.pending, status.running, status.completed).flatten().reversed()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = inflater.inflate(R.layout.etm_transfer_list_item, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return transfers.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(vh: ViewHolder, position: Int) {
|
||||
val transfer = transfers[position]
|
||||
|
||||
val transferTypeStrId = when (transfer.request) {
|
||||
is DownloadRequest -> R.string.etm_transfer_type_download
|
||||
is UploadRequest -> R.string.etm_transfer_type_upload
|
||||
}
|
||||
|
||||
val transferTypeIconId = when (transfer.request) {
|
||||
is DownloadRequest -> R.drawable.ic_cloud_download
|
||||
is UploadRequest -> R.drawable.ic_cloud_upload
|
||||
}
|
||||
|
||||
vh.type.setText(transferTypeStrId)
|
||||
vh.typeIcon.setImageResource(transferTypeIconId)
|
||||
vh.uuid.text = transfer.uuid.toString()
|
||||
vh.path.text = transfer.request.file.remotePath
|
||||
vh.user.text = transfer.request.user.accountName
|
||||
vh.state.text = transfer.state.toString()
|
||||
if (transfer.progress >= 0) {
|
||||
vh.progressEnabled = true
|
||||
vh.progress.text = transfer.progress.toString()
|
||||
} else {
|
||||
vh.progressEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var adapter: Adapter
|
||||
private lateinit var list: RecyclerView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_etm_downloader, container, false)
|
||||
adapter = Adapter(inflater)
|
||||
list = view.findViewById(R.id.etm_download_list)
|
||||
list.layoutManager = LinearLayoutManager(context)
|
||||
list.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
list.adapter = adapter
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
vm.transferManagerConnection.bind()
|
||||
vm.transferManagerConnection.registerStatusListener(this::onDownloaderStatusChanged)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
vm.transferManagerConnection.unbind()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.fragment_etm_file_transfer, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.etm_test_download -> {
|
||||
scheduleTestDownload()
|
||||
true
|
||||
}
|
||||
R.id.etm_test_upload -> {
|
||||
scheduleTestUpload()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleTestDownload() {
|
||||
val request = DownloadRequest(
|
||||
vm.currentUser,
|
||||
OCFile(TEST_DOWNLOAD_DUMMY_PATH),
|
||||
true
|
||||
)
|
||||
vm.transferManagerConnection.enqueue(request)
|
||||
}
|
||||
|
||||
private fun scheduleTestUpload() {
|
||||
val request = UploadRequest(
|
||||
vm.currentUser,
|
||||
OCUpload(TEST_DOWNLOAD_DUMMY_PATH, TEST_DOWNLOAD_DUMMY_PATH, vm.currentUser.accountName),
|
||||
true
|
||||
)
|
||||
vm.transferManagerConnection.enqueue(request)
|
||||
}
|
||||
|
||||
private fun onDownloaderStatusChanged(status: TransferManager.Status) {
|
||||
adapter.setStatus(status)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue