Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,29 @@
android:name=".providers.FileContentProvider"
android:authorities="@string/authority"
android:enabled="true"
android:exported="false"
android:exported="true"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to make this existing Provider public?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, documents provider is queried by external application, and at that point its outside owncloud permission so exported content provider is required.

android:label="@string/sync_string_files"
android:syncable="true" />

<provider
android:name=".providers.UsersAndGroupsSearchProvider"
android:authorities="com.owncloud.android.providers.UsersAndGroupsSearchProvider"
android:authorities=".providers.UsersAndGroupsSearchProvider"
android:enabled="true"
android:exported="false"
android:label="@string/search_users_and_groups_hint" />

<provider
android:authorities="@string/document_provider_authority"
android:name=".providers.DocumentsStorageProvider"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"
android:enabled="@bool/atLeastKitKat">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>

<activity
android:name=".authentication.AuthenticatorActivity"
android:exported="true"
Expand Down
22 changes: 22 additions & 0 deletions res/values-v19/bools.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
ownCloud Android client application

Copyright (C) 2015 ownCloud Inc.
Copyright (C) 2015 Bartosz Przybylski

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2,
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<bool name="atLeastKitKat">true</bool>
</resources>
1 change: 1 addition & 0 deletions res/values/bools.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@
<!-- Default boolean values -->
<resources>
<bool name="large_land_layout">false</bool>
<bool name="atLeastKitKat">false</bool>
</resources>
1 change: 1 addition & 0 deletions res/values/setup.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<string name="app_name">ownCloud</string>
<string name="account_type">owncloud</string> <!-- better if was a domain name; but changing it now would require migrate accounts when the app is updated -->
<string name="authority">org.owncloud</string> <!-- better if was the app package with ".provider" appended ; it identifies the provider -->
<string name="document_provider_authority">org.owncloud.documents</string>
<string name ="db_file">owncloud.db</string>
<string name ="db_name">ownCloud</string>
<string name ="data_folder">owncloud</string>
Expand Down
15 changes: 8 additions & 7 deletions src/com/owncloud/android/authentication/AccountUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ public class AccountUtils {
* account). If none is available and valid, returns null.
*/
public static Account getCurrentOwnCloudAccount(Context context) {
Account[] ocAccounts = AccountManager.get(context).getAccountsByType(
MainApp.getAccountType());
Account[] ocAccounts = getAccounts(context);
Account defaultAccount = null;

SharedPreferences appPreferences = PreferenceManager
Expand All @@ -83,10 +82,14 @@ public static Account getCurrentOwnCloudAccount(Context context) {
return defaultAccount;
}

public static Account[] getAccounts(Context context) {
AccountManager accountManager = AccountManager.get(context);
return accountManager.getAccountsByType(MainApp.getAccountType());
}


public static boolean exists(Account account, Context context) {
Account[] ocAccounts = AccountManager.get(context).getAccountsByType(
MainApp.getAccountType());
Account[] ocAccounts = getAccounts(context);

if (account != null && account.name != null) {
int lastAtPos = account.name.lastIndexOf("@");
Expand Down Expand Up @@ -128,10 +131,8 @@ public static Account getOwnCloudAccountByName(Context context, String accountNa
public static boolean setCurrentOwnCloudAccount(Context context, String accountName) {
boolean result = false;
if (accountName != null) {
Account[] ocAccounts = AccountManager.get(context).getAccountsByType(
MainApp.getAccountType());
boolean found;
for (Account account : ocAccounts) {
for (Account account : getAccounts(context)) {
found = (account.name.equals(accountName));
if (found) {
SharedPreferences.Editor appPrefs = PreferenceManager
Expand Down
209 changes: 209 additions & 0 deletions src/com/owncloud/android/providers/DocumentsStorageProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* ownCloud Android client application
*
* @author Bartosz Przybylski
* Copyright (C) 2015 Bartosz Przybylski
* Copyright (C) 2015 ownCloud Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package com.owncloud.android.providers;

import android.accounts.Account;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Point;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsProvider;

import com.owncloud.android.authentication.AccountUtils;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.providers.cursors.FileCursor;
import com.owncloud.android.providers.cursors.RootCursor;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

@TargetApi(Build.VERSION_CODES.KITKAT)
public class DocumentsStorageProvider extends DocumentsProvider {

private FileDataStorageManager mCurrentStorageManager = null;
private static Map<Long, FileDataStorageManager> mRootIdToStorageManager;

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
initiateStorageMap();

final RootCursor result = new RootCursor(projection);

for (Account account : AccountUtils.getAccounts(getContext()))
result.addRoot(account, getContext());

return result;
}

@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final long docId = Long.parseLong(documentId);
updateCurrentStorageManagerIfNeeded(docId);

final FileCursor result = new FileCursor(projection);
if (result != null)
result.addFile(mCurrentStorageManager.getFileById(docId));

return result;
}

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException {

final long folderId = Long.parseLong(parentDocumentId);
updateCurrentStorageManagerIfNeeded(folderId);

final FileCursor result = new FileCursor(projection);

final OCFile browsedDir = mCurrentStorageManager.getFileById(folderId);
for (OCFile file : mCurrentStorageManager.getFolderContent(browsedDir))
result.addFile(file);

return result;
}

@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal cancellationSignal)
throws FileNotFoundException {
final long docId = Long.parseLong(documentId);
updateCurrentStorageManagerIfNeeded(docId);

OCFile file = mCurrentStorageManager.getFileById(docId);

if (!file.isDown()) {

Intent i = new Intent(getContext(), FileDownloader.class);
i.putExtra(FileDownloader.EXTRA_ACCOUNT, mCurrentStorageManager.getAccount());
i.putExtra(FileDownloader.EXTRA_FILE, file);
getContext().startService(i);

do {
if (!waitOrGetCancelled(cancellationSignal))
return null;
file = mCurrentStorageManager.getFileById(docId);

} while (!file.isDown());
}

return ParcelFileDescriptor.open(
new File(file.getStoragePath()), ParcelFileDescriptor.MODE_READ_ONLY);
}

@Override
public boolean onCreate() {
return true;
}

@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
long docId = Long.parseLong(documentId);
updateCurrentStorageManagerIfNeeded(docId);

OCFile file = mCurrentStorageManager.getFileById(docId);

File realFile = new File(file.getStoragePath());

return new AssetFileDescriptor(
ParcelFileDescriptor.open(realFile, ParcelFileDescriptor.MODE_READ_ONLY),
0,
AssetFileDescriptor.UNKNOWN_LENGTH);
}

@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
updateCurrentStorageManagerIfNeeded(rootId);

OCFile root = mCurrentStorageManager.getFileByPath("/");
FileCursor result = new FileCursor(projection);

for (OCFile f : findFiles(root, query))
result.addFile(f);

return result;
}

private void updateCurrentStorageManagerIfNeeded(long docId) {
if (mCurrentStorageManager == null ||
(mRootIdToStorageManager.containsKey(docId) &&
mCurrentStorageManager != mRootIdToStorageManager.get(docId))) {
mCurrentStorageManager = mRootIdToStorageManager.get(docId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It tries to get a map element in an unsafe mode, because the map could be not instantiate and results in a NullPointerException.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map is always instantiated because of how DocumentsProvider is calling methods in this class. It is guaranteed that queryRoots will always be called before anything else and it is the place where the map is instantiated.

}
}

private void updateCurrentStorageManagerIfNeeded(String rootId) {
for (FileDataStorageManager data : mRootIdToStorageManager.values())
if (data.getAccount().name.equals(rootId))
mCurrentStorageManager = data;
}

private void initiateStorageMap() {

mRootIdToStorageManager = new HashMap<Long, FileDataStorageManager>();

ContentResolver contentResolver = getContext().getContentResolver();

for (Account account : AccountUtils.getAccounts(getContext())) {
final FileDataStorageManager storageManager =
new FileDataStorageManager(account, contentResolver);
final OCFile rootDir = storageManager.getFileByPath("/");
mRootIdToStorageManager.put(rootDir.getFileId(), storageManager);
}

}

private boolean waitOrGetCancelled(CancellationSignal cancellationSignal) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
return false;
}

if (cancellationSignal != null && cancellationSignal.isCanceled())
return false;

return true;
}

Vector<OCFile> findFiles(OCFile root, String query) {
Vector<OCFile> result = new Vector<OCFile>();
for (OCFile f : mCurrentStorageManager.getFolderContent(root)) {
if (f.isFolder()) {
result.addAll(findFiles(f, query));
} else {
if (f.getFileName().contains(query))
result.add(f);
}
}
return result;
}
}
61 changes: 61 additions & 0 deletions src/com/owncloud/android/providers/cursors/FileCursor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* ownCloud Android client application
*
* @author Bartosz Przybylski
* Copyright (C) 2015 Bartosz Przybylski
* Copyright (C) 2015 ownCloud Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package com.owncloud.android.providers.cursors;

import android.annotation.TargetApi;
import android.database.MatrixCursor;
import android.os.Build;
import android.provider.DocumentsContract.Document;

import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.utils.MimetypeIconUtil;

@TargetApi(Build.VERSION_CODES.KITKAT)
public class FileCursor extends MatrixCursor {

private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE,
Document.COLUMN_FLAGS, Document.COLUMN_LAST_MODIFIED
};

public FileCursor(String[] projection) {
super(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
}

public void addFile(OCFile file) {
if (file == null) return;

final int iconRes = MimetypeIconUtil.getFileTypeIconId(file.getMimetype(), file.getFileName());
final String mimeType = file.isFolder() ? Document.MIME_TYPE_DIR : file.getMimetype();
final String imagePath = file.isImage() && file.isDown() ? file.getStoragePath() : null;
int flags = imagePath != null ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;

newRow().add(Document.COLUMN_DOCUMENT_ID, Long.toString(file.getFileId()))
.add(Document.COLUMN_DISPLAY_NAME, file.getFileName())
.add(Document.COLUMN_LAST_MODIFIED, file.getModificationTimestamp())
.add(Document.COLUMN_SIZE, file.getFileLength())
.add(Document.COLUMN_FLAGS, flags)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the "FLAG_SUPPORTS_THUMBNAIL" flag does not imply implement both "buildSearchDocumentsUri" and
"querySearchDocuments" methods?

http://developer.android.com/intl/es/reference/android/provider/DocumentsContract.Root.html#FLAG_SUPPORTS_SEARCH

I'm not sure about it but maybe both are expected (not only one) when that flag is set. Please correct me if I'm wrong ;)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you need to tell the caller that it is safe to call those methods.
In default implementation exception is thrown which is crashing the access to resources

.add(Document.COLUMN_ICON, iconRes)
.add(Document.COLUMN_MIME_TYPE, mimeType);
}
}
Loading