repo created

This commit is contained in:
Fr4nz D13trich 2025-09-18 17:54:51 +02:00
commit 1ef725ef20
2483 changed files with 278273 additions and 0 deletions

View file

@ -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
}
}

View 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
}
}

View file

@ -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;
}
}

View 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();
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}
}

View file

@ -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?
)

View file

@ -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)
}

View file

@ -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)
}
}

View 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
)
}
}

View file

@ -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)
}
}

View file

@ -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");
}
}

View 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)
}
}

View file

@ -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)
}
}

View 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)
}
}
}

View 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
}

View file

@ -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);
}

View file

@ -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);
}
}

View 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
}

View file

@ -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"
}
}
}

View file

@ -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()
}
}

View file

@ -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 }
}
}
}

View file

@ -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()
)
}
)
}

View file

@ -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 = {})
}

View file

@ -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
)
}
}

View file

@ -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"
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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>
}

View file

@ -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))
*/
}
}

View file

@ -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,
"",
""
)
) {
}
}

View file

@ -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),
"",
""
)
) {
}
}

View file

@ -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))
}
}
}
}

View 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
}

View 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()
}

View 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
}

View 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()
}

View 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()

View file

@ -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
}
}

View file

@ -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
}
}

View 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)
}
}

View 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
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
}
}

View file

@ -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()
}
}

View file

@ -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!!
}
}
}

View file

@ -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)
}

View file

@ -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>
}

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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
}
}
}
}

View file

@ -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)
}
}

View file

@ -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
}

View 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)
}
}

View file

@ -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
)
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}
}

View 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();
}
}

View 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();
}
}

View 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();
}

View file

@ -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
}

View file

@ -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)
}
}
}
}

View 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 {}

View file

@ -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
);
}
}

View 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()
}
}
}

View file

@ -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
}
}

View 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>)

View 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
}

View 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;

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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
}
}
}

View file

@ -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("|")
}
}

View file

@ -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)
}
}

View 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))
}
}
}

View file

@ -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
}
}

View 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
}
}

View 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>)

View file

@ -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)
}
}

View 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()
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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