Loading app/src/main/java/it/niedermann/owncloud/notes/persistence/ShareRepository.kt +21 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package it.niedermann.owncloud.notes.persistence import android.app.Application import android.content.Context import android.util.Log import com.nextcloud.android.sso.api.EmptyResponse import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.resources.shares.OCShare Loading @@ -9,7 +10,10 @@ import com.owncloud.android.lib.resources.shares.ShareType import io.reactivex.Single import io.reactivex.schedulers.Schedulers import it.niedermann.owncloud.notes.persistence.entity.Note import it.niedermann.owncloud.notes.share.model.ShareesData import it.niedermann.owncloud.notes.shared.model.ApiVersion import org.json.JSONObject import java.util.ArrayList class ShareRepository private constructor(private val applicationContext: Context) { Loading @@ -28,6 +32,23 @@ class ShareRepository private constructor(private val applicationContext: Contex }.subscribeOn(Schedulers.io()) } fun getSharees( account: SingleSignOnAccount, searchString: String, page: Int, perPage: Int ): Single<ShareesData> { return Single.fromCallable { val shareAPI = apiProvider.getShareAPI(applicationContext, account) val call2 = shareAPI.getSharees2(search = searchString, page = page, perPage = perPage) val response2 = call2.execute() val call = shareAPI.getSharees(search = searchString, page = page, perPage = perPage) val response = call.execute() response.body()?.ocs?.data ?: throw RuntimeException("No shares available") }.subscribeOn(Schedulers.io()) } fun getShares( account: SingleSignOnAccount, remoteId: Long Loading app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt +22 −0 Original line number Diff line number Diff line Loading @@ -3,14 +3,36 @@ package it.niedermann.owncloud.notes.persistence.sync import com.nextcloud.android.sso.api.EmptyResponse import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType import it.niedermann.owncloud.notes.share.model.ShareesData import it.niedermann.owncloud.notes.shared.model.OcsResponse import retrofit2.Call import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Query interface ShareAPI { @GET("sharees") fun getSharees2( @Query("format") format: String = "json", @Query("itemType") itemType: String = "note", @Query("search") search: String, @Query("page") page: Int, @Query("perPage") perPage: Int, @Query("lookup") lookup: Boolean = true, ): Call<Any> @GET("sharees") fun getSharees( @Query("format") format: String = "json", @Query("itemType") itemType: String = "note", @Query("search") search: String, @Query("page") page: Int, @Query("perPage") perPage: Int, @Query("lookup") lookup: Boolean = true, ): Call<OcsResponse<ShareesData>> @GET("shares") fun getShares(remoteId: Long): Call<OcsResponse<List<OCShare>>> Loading app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java +9 −7 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import it.niedermann.owncloud.notes.branding.BrandedActivity; import it.niedermann.owncloud.notes.branding.BrandedSnackbar; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.ActivityNoteShareBinding; import it.niedermann.owncloud.notes.persistence.ShareRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.share.adapter.ShareeListAdapter; Loading @@ -56,6 +57,7 @@ import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment; import it.niedermann.owncloud.notes.share.helper.UsersAndGroupsSearchProvider; import it.niedermann.owncloud.notes.share.listener.FileDetailsSharingMenuBottomSheetActions; import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; import it.niedermann.owncloud.notes.share.model.ShareesData; import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig; import it.niedermann.owncloud.notes.share.operations.ClientFactoryImpl; import it.niedermann.owncloud.notes.share.operations.RetrieveHoverCardAsyncTask; Loading Loading @@ -146,8 +148,7 @@ public class NoteShareActivity extends BrandedActivity implements ShareeListAdap } private void setupSearchView(@Nullable SearchManager searchManager, ComponentName componentName) { private void setupSearchView(@Nullable SearchManager searchManager, ComponentName componentName) { if (searchManager == null) { binding.searchView.setVisibility(View.GONE); return; Loading @@ -162,8 +163,8 @@ public class NoteShareActivity extends BrandedActivity implements ShareeListAdap // avoid fullscreen with softkeyboard binding.searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); UsersAndGroupsSearchProvider provider = new UsersAndGroupsSearchProvider(account, clientFactory.create()); ShareRepository repository = ShareRepository.getInstance(getApplicationContext()); UsersAndGroupsSearchProvider provider = new UsersAndGroupsSearchProvider(this, account, repository); binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { Loading @@ -174,9 +175,10 @@ public class NoteShareActivity extends BrandedActivity implements ShareeListAdap @Override public boolean onQueryTextChange(String newText) { Log_OC.e(NoteShareActivity.class.getSimpleName(), "Failed to pick email address as Cursor is null." + newText); // leave it for the parent listener in the hierarchy / default behaviour new Thread(() -> {{ ShareesData data = provider.searchForUsersOrGroups(newText); Log_OC.e(NoteShareActivity.class.getSimpleName(), "Fetched" + newText); }}).start(); return false; } }); Loading app/src/main/java/it/niedermann/owncloud/notes/share/helper/UsersAndGroupsSearchProvider.java +38 −283 Original line number Diff line number Diff line package it.niedermann.owncloud.notes.share.helper; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_CLEAR_AT; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_ICON; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_MESSAGE; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_STATUS; import android.app.SearchManager; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation; import com.owncloud.android.lib.resources.shares.ShareType; Loading @@ -36,7 +36,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; Loading @@ -44,25 +43,20 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutionException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_CLEAR_AT; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_ICON; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_MESSAGE; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_STATUS; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.persistence.ApiProvider; import it.niedermann.owncloud.notes.persistence.ShareRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.share.model.ShareesData; import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig; import it.niedermann.owncloud.notes.shared.model.ApiVersion; /** * Content provider for search suggestions, to search for users and groups existing in an ownCloud server. */ public class UsersAndGroupsSearchProvider extends ContentProvider { public class UsersAndGroupsSearchProvider { private static final String TAG = UsersAndGroupsSearchProvider.class.getSimpleName(); Loading Loading @@ -93,40 +87,17 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { private UriMatcher mUriMatcher; private OwnCloudClient client; private ShareRepository repository; private Account account; private Context context; public UsersAndGroupsSearchProvider(Account account, OwnCloudClient client) { public UsersAndGroupsSearchProvider(Context context, Account account, ShareRepository repository) { this.context = context; this.account = account; this.client = client; } private static final Map<String, ShareType> sShareTypes = new HashMap<>(); public static ShareType getShareType(String authority) { return sShareTypes.get(authority); } private static void setActionShareWith(@NonNull Context context) { ACTION_SHARE_WITH = context.getString(R.string.users_and_groups_share_with); } @Nullable @Override public String getType(@NonNull Uri uri) { // TODO implement return null; } this.repository = repository; @Override public boolean onCreate() { if (getContext() == null) { return false; } AUTHORITY = getContext().getString(R.string.users_and_groups_search_authority); setActionShareWith(getContext()); AUTHORITY = context.getString(R.string.users_and_groups_search_authority); setActionShareWith(context); DATA_USER = AUTHORITY + ".data.user"; DATA_GROUP = AUTHORITY + ".data.group"; DATA_ROOM = AUTHORITY + ".data.room"; Loading @@ -143,223 +114,33 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); mUriMatcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH); return true; } /** * returns sharee from server * * Reference: http://developer.android.com/guide/topics/search/adding-custom-suggestions.html#CustomContentProvider * * @param uri Content {@link Uri}, formatted as "content://com.nextcloud.android.providers.UsersAndGroupsSearchProvider/" * + {@link android.app.SearchManager#SUGGEST_URI_PATH_QUERY} + "/" + * 'userQuery' * @param projection Expected to be NULL. * @param selection Expected to be NULL. * @param selectionArgs Expected to be NULL. * @param sortOrder Expected to be NULL. * @return Cursor with possible sharees in the server that match 'query'. */ @Nullable @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Log_OC.d(TAG, "query received in thread " + Thread.currentThread().getName()); int match = mUriMatcher.match(uri); if (match == SEARCH) { return searchForUsersOrGroups(uri); } return null; } private Cursor searchForUsersOrGroups(Uri uri) { // TODO check searchConfig and filter results Log.d(TAG, "searchForUsersOrGroups: searchConfig only users: " + UsersAndGroupsSearchConfig.INSTANCE.getSearchOnlyUsers()); String lastPathSegment = uri.getLastPathSegment(); if (lastPathSegment == null) { throw new IllegalArgumentException("Wrong URI passed!"); } String userQuery = lastPathSegment.toLowerCase(Locale.ROOT); // ApiProvider.getInstance().getFilesAPI(getContext(), account, ApiVersion.API_VERSION_1_0).getDirectEditingInfo(); // request to the OC server about users and groups matching userQuery GetShareesRemoteOperation searchRequest = new GetShareesRemoteOperation(userQuery, REQUESTED_PAGE, RESULTS_PER_PAGE); RemoteOperationResult<ArrayList<JSONObject>> result = searchRequest.execute(client); List<JSONObject> names = new ArrayList<>(); private static final Map<String, ShareType> sShareTypes = new HashMap<>(); if (result.isSuccess()) { names = result.getResultData(); } else { showErrorMessage(result); public static ShareType getShareType(String authority) { return sShareTypes.get(authority); } MatrixCursor response = null; // convert the responses from the OC server to the expected format if (names.size() > 0) { if (getContext() == null) { throw new IllegalArgumentException("Context may not be null!"); private static void setActionShareWith(@NonNull Context context) { ACTION_SHARE_WITH = context.getString(R.string.users_and_groups_share_with); } response = new MatrixCursor(COLUMNS); Uri userBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_USER).build(); Uri groupBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_GROUP).build(); Uri roomBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_ROOM).build(); Uri remoteBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_REMOTE).build(); Uri emailBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_EMAIL).build(); Uri circleBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_CIRCLE).build(); public ShareesData searchForUsersOrGroups(String userQuery) { final SingleSignOnAccount ssoAcc; try { Iterator<JSONObject> namesIt = names.iterator(); JSONObject item; String displayName; String subline = null; Object icon = 0; Uri dataUri; int count = 0; while (namesIt.hasNext()) { item = namesIt.next(); dataUri = null; displayName = null; String userName = item.getString(GetShareesRemoteOperation.PROPERTY_LABEL); String name = item.isNull("name") ? "" : item.getString("name"); JSONObject value = item.getJSONObject(GetShareesRemoteOperation.NODE_VALUE); ShareType type = ShareType.fromValue(value.getInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE)); String shareWith = value.getString(GetShareesRemoteOperation.PROPERTY_SHARE_WITH); Status status; JSONObject statusObject = item.optJSONObject(PROPERTY_STATUS); if (statusObject != null) { status = new Status( StatusType.valueOf(statusObject.getString(PROPERTY_STATUS).toUpperCase(Locale.US)), statusObject.isNull(PROPERTY_MESSAGE) ? "" : statusObject.getString(PROPERTY_MESSAGE), statusObject.isNull(PROPERTY_ICON) ? "" : statusObject.getString(PROPERTY_ICON), statusObject.isNull(PROPERTY_CLEAR_AT) ? -1 : statusObject.getLong(PROPERTY_CLEAR_AT)); } else { status = new Status(StatusType.OFFLINE, "", "", -1); } if ( UsersAndGroupsSearchConfig.INSTANCE.getSearchOnlyUsers() && type != ShareType.USER) { // skip all types but users, as E2E secure share is only allowed to users on same server continue; } switch (type) { case GROUP: displayName = userName; icon = R.drawable.ic_group; dataUri = Uri.withAppendedPath(groupBaseUri, shareWith); break; case FEDERATED: if (true) { icon = R.drawable.ic_account_circle_grey_24dp; dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith); if (userName.equals(shareWith)) { displayName = name; subline = getContext().getString(R.string.remote); } else { String[] uriSplitted = shareWith.split("@"); displayName = name; subline = getContext().getString(R.string.share_known_remote_on_clarification, uriSplitted[uriSplitted.length - 1]); } } break; case USER: displayName = userName; subline = (status.getMessage() == null || status.getMessage().isEmpty()) ? null : status.getMessage(); Uri.Builder builder = Uri.parse("content://" + AUTHORITY + "/icon").buildUpon(); builder.appendQueryParameter("shareWith", shareWith); builder.appendQueryParameter("displayName", displayName); builder.appendQueryParameter("status", status.getStatus().toString()); if (!TextUtils.isEmpty(status.getIcon()) && !"null".equals(status.getIcon())) { builder.appendQueryParameter("icon", status.getIcon()); } icon = builder.build(); dataUri = Uri.withAppendedPath(userBaseUri, shareWith); break; case EMAIL: icon = R.drawable.ic_email; displayName = name; subline = shareWith; dataUri = Uri.withAppendedPath(emailBaseUri, shareWith); break; case ROOM: icon = R.drawable.ic_talk; displayName = userName; dataUri = Uri.withAppendedPath(roomBaseUri, shareWith); break; case CIRCLE: icon = R.drawable.ic_circles; displayName = userName; dataUri = Uri.withAppendedPath(circleBaseUri, shareWith); break; default: break; } if (displayName != null && dataUri != null) { response.newRow() .add(count++) // BaseColumns._ID .add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_1 .add(subline) // SearchManager.SUGGEST_COLUMN_TEXT_2 .add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1 .add(dataUri); } } } catch (JSONException e) { Log_OC.e(TAG, "Exception while parsing data of users/groups", e); } } return response; ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(context); return repository.getSharees(ssoAcc, userQuery, REQUESTED_PAGE, RESULTS_PER_PAGE).blockingGet(); } catch (Exception e) { Log_OC.e(TAG, "Exception while searching", e); } @Nullable @Override public Uri insert(@NonNull Uri uri, ContentValues values) { return null; } @Override public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Nullable @Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { try { Bitmap avatar = Glide.with(getContext()) Bitmap avatar = Glide.with(context) .asBitmap() .load(new SingleSignOnUrl(account.getAccountName(), account.getUrl() + "/index.php/avatar/" + Uri.encode(account.getUserName()) + "/64")) .placeholder(R.drawable.ic_account_circle_grey_24dp) Loading @@ -369,7 +150,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { .get(); // create a file to write bitmap data File f = new File(getContext().getCacheDir(), "test"); File f = new File(context.getCacheDir(), "test"); try { if (f.exists()) { if (!f.delete()) { Loading Loading @@ -404,30 +185,4 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { throw new RuntimeException(e); } } /** * Show error message * * @param result Result with the failure information. */ private void showErrorMessage(final RemoteOperationResult result) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(() -> { // The Toast must be shown in the main thread to grant that will be hidden correctly; otherwise // the thread may die before, an exception will occur, and the message will be left on the screen // until the app dies Context context = getContext(); if (context == null) { throw new IllegalArgumentException("Context may not be null!"); } Toast.makeText(getContext().getApplicationContext(), result.getMessage(), Toast.LENGTH_SHORT).show(); }); } } app/src/main/java/it/niedermann/owncloud/notes/share/model/ShareesData.kt 0 → 100644 +34 −0 Original line number Diff line number Diff line package it.niedermann.owncloud.notes.share.model data class ShareesData( val exact: ExactMatches, val users: List<ShareeItem>, val groups: List<ShareeItem>, val remotes: List<ShareeItem>, val remote_groups: List<ShareeItem>, val emails: List<ShareeItem>, val circles: List<ShareeItem>, val rooms: List<ShareeItem>, val lookup: List<ShareeItem>, val lookupEnabled: Boolean ) data class ExactMatches( val users: List<ShareeItem>, val groups: List<ShareeItem>, val remotes: List<ShareeItem>, val remote_groups: List<ShareeItem>, val emails: List<ShareeItem>, val circles: List<ShareeItem>, val rooms: List<ShareeItem> ) data class ShareeItem( val label: String, val value: ShareeValue ) data class ShareeValue( val shareType: Double, val shareWith: String ) Loading
app/src/main/java/it/niedermann/owncloud/notes/persistence/ShareRepository.kt +21 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package it.niedermann.owncloud.notes.persistence import android.app.Application import android.content.Context import android.util.Log import com.nextcloud.android.sso.api.EmptyResponse import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.resources.shares.OCShare Loading @@ -9,7 +10,10 @@ import com.owncloud.android.lib.resources.shares.ShareType import io.reactivex.Single import io.reactivex.schedulers.Schedulers import it.niedermann.owncloud.notes.persistence.entity.Note import it.niedermann.owncloud.notes.share.model.ShareesData import it.niedermann.owncloud.notes.shared.model.ApiVersion import org.json.JSONObject import java.util.ArrayList class ShareRepository private constructor(private val applicationContext: Context) { Loading @@ -28,6 +32,23 @@ class ShareRepository private constructor(private val applicationContext: Contex }.subscribeOn(Schedulers.io()) } fun getSharees( account: SingleSignOnAccount, searchString: String, page: Int, perPage: Int ): Single<ShareesData> { return Single.fromCallable { val shareAPI = apiProvider.getShareAPI(applicationContext, account) val call2 = shareAPI.getSharees2(search = searchString, page = page, perPage = perPage) val response2 = call2.execute() val call = shareAPI.getSharees(search = searchString, page = page, perPage = perPage) val response = call.execute() response.body()?.ocs?.data ?: throw RuntimeException("No shares available") }.subscribeOn(Schedulers.io()) } fun getShares( account: SingleSignOnAccount, remoteId: Long Loading
app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt +22 −0 Original line number Diff line number Diff line Loading @@ -3,14 +3,36 @@ package it.niedermann.owncloud.notes.persistence.sync import com.nextcloud.android.sso.api.EmptyResponse import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType import it.niedermann.owncloud.notes.share.model.ShareesData import it.niedermann.owncloud.notes.shared.model.OcsResponse import retrofit2.Call import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Query interface ShareAPI { @GET("sharees") fun getSharees2( @Query("format") format: String = "json", @Query("itemType") itemType: String = "note", @Query("search") search: String, @Query("page") page: Int, @Query("perPage") perPage: Int, @Query("lookup") lookup: Boolean = true, ): Call<Any> @GET("sharees") fun getSharees( @Query("format") format: String = "json", @Query("itemType") itemType: String = "note", @Query("search") search: String, @Query("page") page: Int, @Query("perPage") perPage: Int, @Query("lookup") lookup: Boolean = true, ): Call<OcsResponse<ShareesData>> @GET("shares") fun getShares(remoteId: Long): Call<OcsResponse<List<OCShare>>> Loading
app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java +9 −7 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import it.niedermann.owncloud.notes.branding.BrandedActivity; import it.niedermann.owncloud.notes.branding.BrandedSnackbar; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.ActivityNoteShareBinding; import it.niedermann.owncloud.notes.persistence.ShareRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.share.adapter.ShareeListAdapter; Loading @@ -56,6 +57,7 @@ import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment; import it.niedermann.owncloud.notes.share.helper.UsersAndGroupsSearchProvider; import it.niedermann.owncloud.notes.share.listener.FileDetailsSharingMenuBottomSheetActions; import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; import it.niedermann.owncloud.notes.share.model.ShareesData; import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig; import it.niedermann.owncloud.notes.share.operations.ClientFactoryImpl; import it.niedermann.owncloud.notes.share.operations.RetrieveHoverCardAsyncTask; Loading Loading @@ -146,8 +148,7 @@ public class NoteShareActivity extends BrandedActivity implements ShareeListAdap } private void setupSearchView(@Nullable SearchManager searchManager, ComponentName componentName) { private void setupSearchView(@Nullable SearchManager searchManager, ComponentName componentName) { if (searchManager == null) { binding.searchView.setVisibility(View.GONE); return; Loading @@ -162,8 +163,8 @@ public class NoteShareActivity extends BrandedActivity implements ShareeListAdap // avoid fullscreen with softkeyboard binding.searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); UsersAndGroupsSearchProvider provider = new UsersAndGroupsSearchProvider(account, clientFactory.create()); ShareRepository repository = ShareRepository.getInstance(getApplicationContext()); UsersAndGroupsSearchProvider provider = new UsersAndGroupsSearchProvider(this, account, repository); binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { Loading @@ -174,9 +175,10 @@ public class NoteShareActivity extends BrandedActivity implements ShareeListAdap @Override public boolean onQueryTextChange(String newText) { Log_OC.e(NoteShareActivity.class.getSimpleName(), "Failed to pick email address as Cursor is null." + newText); // leave it for the parent listener in the hierarchy / default behaviour new Thread(() -> {{ ShareesData data = provider.searchForUsersOrGroups(newText); Log_OC.e(NoteShareActivity.class.getSimpleName(), "Fetched" + newText); }}).start(); return false; } }); Loading
app/src/main/java/it/niedermann/owncloud/notes/share/helper/UsersAndGroupsSearchProvider.java +38 −283 Original line number Diff line number Diff line package it.niedermann.owncloud.notes.share.helper; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_CLEAR_AT; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_ICON; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_MESSAGE; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_STATUS; import android.app.SearchManager; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation; import com.owncloud.android.lib.resources.shares.ShareType; Loading @@ -36,7 +36,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; Loading @@ -44,25 +43,20 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutionException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_CLEAR_AT; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_ICON; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_MESSAGE; import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_STATUS; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.persistence.ApiProvider; import it.niedermann.owncloud.notes.persistence.ShareRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.share.model.ShareesData; import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig; import it.niedermann.owncloud.notes.shared.model.ApiVersion; /** * Content provider for search suggestions, to search for users and groups existing in an ownCloud server. */ public class UsersAndGroupsSearchProvider extends ContentProvider { public class UsersAndGroupsSearchProvider { private static final String TAG = UsersAndGroupsSearchProvider.class.getSimpleName(); Loading Loading @@ -93,40 +87,17 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { private UriMatcher mUriMatcher; private OwnCloudClient client; private ShareRepository repository; private Account account; private Context context; public UsersAndGroupsSearchProvider(Account account, OwnCloudClient client) { public UsersAndGroupsSearchProvider(Context context, Account account, ShareRepository repository) { this.context = context; this.account = account; this.client = client; } private static final Map<String, ShareType> sShareTypes = new HashMap<>(); public static ShareType getShareType(String authority) { return sShareTypes.get(authority); } private static void setActionShareWith(@NonNull Context context) { ACTION_SHARE_WITH = context.getString(R.string.users_and_groups_share_with); } @Nullable @Override public String getType(@NonNull Uri uri) { // TODO implement return null; } this.repository = repository; @Override public boolean onCreate() { if (getContext() == null) { return false; } AUTHORITY = getContext().getString(R.string.users_and_groups_search_authority); setActionShareWith(getContext()); AUTHORITY = context.getString(R.string.users_and_groups_search_authority); setActionShareWith(context); DATA_USER = AUTHORITY + ".data.user"; DATA_GROUP = AUTHORITY + ".data.group"; DATA_ROOM = AUTHORITY + ".data.room"; Loading @@ -143,223 +114,33 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); mUriMatcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH); return true; } /** * returns sharee from server * * Reference: http://developer.android.com/guide/topics/search/adding-custom-suggestions.html#CustomContentProvider * * @param uri Content {@link Uri}, formatted as "content://com.nextcloud.android.providers.UsersAndGroupsSearchProvider/" * + {@link android.app.SearchManager#SUGGEST_URI_PATH_QUERY} + "/" + * 'userQuery' * @param projection Expected to be NULL. * @param selection Expected to be NULL. * @param selectionArgs Expected to be NULL. * @param sortOrder Expected to be NULL. * @return Cursor with possible sharees in the server that match 'query'. */ @Nullable @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Log_OC.d(TAG, "query received in thread " + Thread.currentThread().getName()); int match = mUriMatcher.match(uri); if (match == SEARCH) { return searchForUsersOrGroups(uri); } return null; } private Cursor searchForUsersOrGroups(Uri uri) { // TODO check searchConfig and filter results Log.d(TAG, "searchForUsersOrGroups: searchConfig only users: " + UsersAndGroupsSearchConfig.INSTANCE.getSearchOnlyUsers()); String lastPathSegment = uri.getLastPathSegment(); if (lastPathSegment == null) { throw new IllegalArgumentException("Wrong URI passed!"); } String userQuery = lastPathSegment.toLowerCase(Locale.ROOT); // ApiProvider.getInstance().getFilesAPI(getContext(), account, ApiVersion.API_VERSION_1_0).getDirectEditingInfo(); // request to the OC server about users and groups matching userQuery GetShareesRemoteOperation searchRequest = new GetShareesRemoteOperation(userQuery, REQUESTED_PAGE, RESULTS_PER_PAGE); RemoteOperationResult<ArrayList<JSONObject>> result = searchRequest.execute(client); List<JSONObject> names = new ArrayList<>(); private static final Map<String, ShareType> sShareTypes = new HashMap<>(); if (result.isSuccess()) { names = result.getResultData(); } else { showErrorMessage(result); public static ShareType getShareType(String authority) { return sShareTypes.get(authority); } MatrixCursor response = null; // convert the responses from the OC server to the expected format if (names.size() > 0) { if (getContext() == null) { throw new IllegalArgumentException("Context may not be null!"); private static void setActionShareWith(@NonNull Context context) { ACTION_SHARE_WITH = context.getString(R.string.users_and_groups_share_with); } response = new MatrixCursor(COLUMNS); Uri userBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_USER).build(); Uri groupBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_GROUP).build(); Uri roomBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_ROOM).build(); Uri remoteBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_REMOTE).build(); Uri emailBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_EMAIL).build(); Uri circleBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_CIRCLE).build(); public ShareesData searchForUsersOrGroups(String userQuery) { final SingleSignOnAccount ssoAcc; try { Iterator<JSONObject> namesIt = names.iterator(); JSONObject item; String displayName; String subline = null; Object icon = 0; Uri dataUri; int count = 0; while (namesIt.hasNext()) { item = namesIt.next(); dataUri = null; displayName = null; String userName = item.getString(GetShareesRemoteOperation.PROPERTY_LABEL); String name = item.isNull("name") ? "" : item.getString("name"); JSONObject value = item.getJSONObject(GetShareesRemoteOperation.NODE_VALUE); ShareType type = ShareType.fromValue(value.getInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE)); String shareWith = value.getString(GetShareesRemoteOperation.PROPERTY_SHARE_WITH); Status status; JSONObject statusObject = item.optJSONObject(PROPERTY_STATUS); if (statusObject != null) { status = new Status( StatusType.valueOf(statusObject.getString(PROPERTY_STATUS).toUpperCase(Locale.US)), statusObject.isNull(PROPERTY_MESSAGE) ? "" : statusObject.getString(PROPERTY_MESSAGE), statusObject.isNull(PROPERTY_ICON) ? "" : statusObject.getString(PROPERTY_ICON), statusObject.isNull(PROPERTY_CLEAR_AT) ? -1 : statusObject.getLong(PROPERTY_CLEAR_AT)); } else { status = new Status(StatusType.OFFLINE, "", "", -1); } if ( UsersAndGroupsSearchConfig.INSTANCE.getSearchOnlyUsers() && type != ShareType.USER) { // skip all types but users, as E2E secure share is only allowed to users on same server continue; } switch (type) { case GROUP: displayName = userName; icon = R.drawable.ic_group; dataUri = Uri.withAppendedPath(groupBaseUri, shareWith); break; case FEDERATED: if (true) { icon = R.drawable.ic_account_circle_grey_24dp; dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith); if (userName.equals(shareWith)) { displayName = name; subline = getContext().getString(R.string.remote); } else { String[] uriSplitted = shareWith.split("@"); displayName = name; subline = getContext().getString(R.string.share_known_remote_on_clarification, uriSplitted[uriSplitted.length - 1]); } } break; case USER: displayName = userName; subline = (status.getMessage() == null || status.getMessage().isEmpty()) ? null : status.getMessage(); Uri.Builder builder = Uri.parse("content://" + AUTHORITY + "/icon").buildUpon(); builder.appendQueryParameter("shareWith", shareWith); builder.appendQueryParameter("displayName", displayName); builder.appendQueryParameter("status", status.getStatus().toString()); if (!TextUtils.isEmpty(status.getIcon()) && !"null".equals(status.getIcon())) { builder.appendQueryParameter("icon", status.getIcon()); } icon = builder.build(); dataUri = Uri.withAppendedPath(userBaseUri, shareWith); break; case EMAIL: icon = R.drawable.ic_email; displayName = name; subline = shareWith; dataUri = Uri.withAppendedPath(emailBaseUri, shareWith); break; case ROOM: icon = R.drawable.ic_talk; displayName = userName; dataUri = Uri.withAppendedPath(roomBaseUri, shareWith); break; case CIRCLE: icon = R.drawable.ic_circles; displayName = userName; dataUri = Uri.withAppendedPath(circleBaseUri, shareWith); break; default: break; } if (displayName != null && dataUri != null) { response.newRow() .add(count++) // BaseColumns._ID .add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_1 .add(subline) // SearchManager.SUGGEST_COLUMN_TEXT_2 .add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1 .add(dataUri); } } } catch (JSONException e) { Log_OC.e(TAG, "Exception while parsing data of users/groups", e); } } return response; ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(context); return repository.getSharees(ssoAcc, userQuery, REQUESTED_PAGE, RESULTS_PER_PAGE).blockingGet(); } catch (Exception e) { Log_OC.e(TAG, "Exception while searching", e); } @Nullable @Override public Uri insert(@NonNull Uri uri, ContentValues values) { return null; } @Override public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Nullable @Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { try { Bitmap avatar = Glide.with(getContext()) Bitmap avatar = Glide.with(context) .asBitmap() .load(new SingleSignOnUrl(account.getAccountName(), account.getUrl() + "/index.php/avatar/" + Uri.encode(account.getUserName()) + "/64")) .placeholder(R.drawable.ic_account_circle_grey_24dp) Loading @@ -369,7 +150,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { .get(); // create a file to write bitmap data File f = new File(getContext().getCacheDir(), "test"); File f = new File(context.getCacheDir(), "test"); try { if (f.exists()) { if (!f.delete()) { Loading Loading @@ -404,30 +185,4 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { throw new RuntimeException(e); } } /** * Show error message * * @param result Result with the failure information. */ private void showErrorMessage(final RemoteOperationResult result) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(() -> { // The Toast must be shown in the main thread to grant that will be hidden correctly; otherwise // the thread may die before, an exception will occur, and the message will be left on the screen // until the app dies Context context = getContext(); if (context == null) { throw new IllegalArgumentException("Context may not be null!"); } Toast.makeText(getContext().getApplicationContext(), result.getMessage(), Toast.LENGTH_SHORT).show(); }); } }
app/src/main/java/it/niedermann/owncloud/notes/share/model/ShareesData.kt 0 → 100644 +34 −0 Original line number Diff line number Diff line package it.niedermann.owncloud.notes.share.model data class ShareesData( val exact: ExactMatches, val users: List<ShareeItem>, val groups: List<ShareeItem>, val remotes: List<ShareeItem>, val remote_groups: List<ShareeItem>, val emails: List<ShareeItem>, val circles: List<ShareeItem>, val rooms: List<ShareeItem>, val lookup: List<ShareeItem>, val lookupEnabled: Boolean ) data class ExactMatches( val users: List<ShareeItem>, val groups: List<ShareeItem>, val remotes: List<ShareeItem>, val remote_groups: List<ShareeItem>, val emails: List<ShareeItem>, val circles: List<ShareeItem>, val rooms: List<ShareeItem> ) data class ShareeItem( val label: String, val value: ShareeValue ) data class ShareeValue( val shareType: Double, val shareWith: String )