Skip to content

Commit 116e42f

Browse files
Merge pull request #14497 from nextcloud/feature/open-in-notes
Tighter integration - Open in Notes Button
2 parents 92a57cf + a76859c commit 116e42f

File tree

72 files changed

+1599
-276
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1599
-276
lines changed

app/schemas/com.nextcloud.client.database.NextcloudDatabase/87.json

Lines changed: 1337 additions & 0 deletions
Large diffs are not rendered by default.

app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ import com.owncloud.android.db.ProviderMeta
6969
AutoMigration(from = 82, to = 83),
7070
AutoMigration(from = 83, to = 84),
7171
AutoMigration(from = 84, to = 85, spec = DatabaseMigrationUtil.DeleteColumnSpec::class),
72-
AutoMigration(from = 85, to = 86, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
72+
AutoMigration(from = 85, to = 86, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
73+
AutoMigration(from = 86, to = 87, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
7374
],
7475
exportSchema = true
7576
)

app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,7 @@ data class CapabilityEntity(
136136
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT)
137137
val filesDownloadLimitDefault: Int?,
138138
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RECOMMENDATION)
139-
val recommendation: Int?
139+
val recommendation: Int?,
140+
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH)
141+
val notesFolderPath: String?
140142
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 ZetaTom <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.utils
9+
10+
import android.content.ActivityNotFoundException
11+
import android.content.Context
12+
import android.content.Intent
13+
import android.net.Uri
14+
import com.nextcloud.client.account.User
15+
import com.owncloud.android.ui.activity.FileDisplayActivity
16+
import java.util.Optional
17+
import kotlin.jvm.optionals.getOrNull
18+
19+
object LinkHelper {
20+
const val APP_NEXTCLOUD_NOTES = "it.niedermann.owncloud.notes"
21+
const val APP_NEXTCLOUD_TALK = "com.nextcloud.talk2"
22+
23+
/**
24+
* Open specified app and, if not installed redirect to corresponding download.
25+
*
26+
* @param packageName of app to be opened
27+
* @param user to pass in intent
28+
*/
29+
fun openAppOrStore(packageName: String, user: Optional<User>, context: Context) {
30+
openAppOrStore(packageName, user.getOrNull(), context)
31+
}
32+
33+
/**
34+
* Open specified app and, if not installed redirect to corresponding download.
35+
*
36+
* @param packageName of app to be opened
37+
* @param user to pass in intent
38+
*/
39+
fun openAppOrStore(packageName: String, user: User?, context: Context) {
40+
val intent = context.packageManager.getLaunchIntentForPackage(packageName)
41+
if (intent != null) {
42+
// app installed - open directly
43+
// TODO handle null user?
44+
intent.putExtra(FileDisplayActivity.KEY_ACCOUNT, user.hashCode())
45+
context.startActivity(intent)
46+
} else {
47+
// app not found - open market (Google Play Store, F-Droid, etc.)
48+
openAppStore(packageName, false, context)
49+
}
50+
}
51+
52+
/**
53+
* Open app store page of specified app or search for specified string. Will attempt to open browser when no app
54+
* store is available.
55+
*
56+
* @param string packageName or url-encoded search string
57+
* @param search false -> show app corresponding to packageName; true -> open search for string
58+
*/
59+
fun openAppStore(string: String, search: Boolean = false, context: Context) {
60+
var suffix = (if (search) "search?q=" else "details?id=") + string
61+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://$suffix"))
62+
try {
63+
context.startActivity(intent)
64+
} catch (activityNotFoundException1: ActivityNotFoundException) {
65+
// all is lost: open google play store web page for app
66+
if (!search) {
67+
suffix = "apps/$suffix"
68+
}
69+
intent.setData(Uri.parse("https://play.google.com/store/$suffix"))
70+
context.startActivity(intent)
71+
}
72+
}
73+
}

app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2315,6 +2315,8 @@ private ContentValues createContentValues(String accountName, OCCapability capab
23152315

23162316
contentValues.put(ProviderTableMeta.CAPABILITIES_RECOMMENDATION, capability.getRecommendations().getValue());
23172317

2318+
contentValues.put(ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH, capability.getNotesFolderPath());
2319+
23182320
return contentValues;
23192321
}
23202322

@@ -2490,7 +2492,10 @@ private OCCapability createCapabilityInstance(Cursor cursor) {
24902492
capability.setForbiddenFilenameBaseNamesJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES));
24912493
capability.setFilesDownloadLimit(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT));
24922494
capability.setFilesDownloadLimitDefault(getInt(cursor, ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT));
2495+
24932496
capability.setRecommendations(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_RECOMMENDATION));
2497+
2498+
capability.setNotesFolderPath(getString(cursor, ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH));
24942499
}
24952500

24962501
return capability;

app/src/main/java/com/owncloud/android/db/ProviderMeta.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*/
2626
public class ProviderMeta {
2727
public static final String DB_NAME = "filelist";
28-
public static final int DB_VERSION = 86;
28+
public static final int DB_VERSION = 87;
2929

3030
private ProviderMeta() {
3131
// No instance
@@ -272,6 +272,7 @@ static public class ProviderTableMeta implements BaseColumns {
272272
public static final String CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES = "forbidden_filename_basenames";
273273
public static final String CAPABILITIES_FILES_DOWNLOAD_LIMIT = "files_download_limit";
274274
public static final String CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT = "files_download_limit_default";
275+
public static final String CAPABILITIES_NOTES_FOLDER_PATH = "notes_folder_path";
275276

276277
//Columns of Uploads table
277278
public static final String UPLOADS_LOCAL_PATH = "local_path";

app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import com.nextcloud.ui.ChooseAccountDialogFragment;
6161
import com.nextcloud.ui.composeActivity.ComposeActivity;
6262
import com.nextcloud.ui.composeActivity.ComposeDestination;
63+
import com.nextcloud.utils.LinkHelper;
6364
import com.nextcloud.utils.extensions.ViewExtensionsKt;
6465
import com.nextcloud.utils.mdm.MDMConfig;
6566
import com.owncloud.android.MainApp;
@@ -425,9 +426,9 @@ private void showTopBanner(LinearLayout banner, int primaryColor) {
425426
LinearLayout moreView = banner.findViewById(R.id.drawer_ecosystem_more);
426427
LinearLayout assistantView = banner.findViewById(R.id.drawer_ecosystem_assistant);
427428

428-
notesView.setOnClickListener(v -> openAppOrStore("it.niedermann.owncloud.notes"));
429-
talkView.setOnClickListener(v -> openAppOrStore("com.nextcloud.talk2"));
430-
moreView.setOnClickListener(v -> openAppStore("Nextcloud", true));
429+
notesView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppOrStore(LinkHelper.APP_NEXTCLOUD_NOTES, getUser(), this));
430+
talkView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppOrStore(LinkHelper.APP_NEXTCLOUD_TALK, getUser(), this));
431+
moreView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppStore("Nextcloud", true, this));
431432
assistantView.setOnClickListener(v -> {
432433
DrawerActivity.menuItemId = Menu.NONE;
433434
startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title);
@@ -459,45 +460,6 @@ private void showTopBanner(LinearLayout banner, int primaryColor) {
459460
banner.setVisibility(View.VISIBLE);
460461
}
461462

462-
/**
463-
* Open specified app and, if not installed redirect to corresponding download.
464-
*
465-
* @param packageName of app to be opened
466-
*/
467-
private void openAppOrStore(String packageName) {
468-
Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
469-
if (intent != null) {
470-
// app installed - open directly
471-
intent.putExtra(FileDisplayActivity.KEY_ACCOUNT, getUser().get().hashCode());
472-
startActivity(intent);
473-
} else {
474-
// app not found - open market (Google Play Store, F-Droid, etc.)
475-
openAppStore(packageName, false);
476-
}
477-
}
478-
479-
/**
480-
* Open app store page of specified app or search for specified string. Will attempt to open browser when no app
481-
* store is available.
482-
*
483-
* @param string packageName or url-encoded search string
484-
* @param search false -> show app corresponding to packageName; true -> open search for string
485-
*/
486-
private void openAppStore(String string, boolean search) {
487-
String suffix = (search ? "search?q=" : "details?id=") + string;
488-
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://" + suffix));
489-
try {
490-
startActivity(intent);
491-
} catch (android.content.ActivityNotFoundException activityNotFoundException1) {
492-
// all is lost: open google play store web page for app
493-
if (!search) {
494-
suffix = "apps/" + suffix;
495-
}
496-
intent.setData(Uri.parse("https://play.google.com/store/" + suffix));
497-
startActivity(intent);
498-
}
499-
}
500-
501463
private void setDrawerHeaderLogo(Drawable drawable, String serverName) {
502464
ImageView imageHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_logo);
503465
imageHeader.setImageDrawable(drawable);
@@ -1357,7 +1319,7 @@ protected void handleDeepLink(@NonNull Uri uri) {
13571319
findViewById(R.id.fab_main).callOnClick();
13581320
break;
13591321
case ACTION_APP_UPDATE:
1360-
openAppStore(getPackageName(), false);
1322+
LinkHelper.INSTANCE.openAppStore(getPackageName(), false, this);
13611323
break;
13621324
case OPEN_NOTIFICATIONS:
13631325
startActivity(NotificationsActivity.class);

app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.nextcloud.client.preferences.AppPreferences;
4040
import com.nextcloud.model.OCFileFilterType;
4141
import com.nextcloud.model.OfflineOperationType;
42+
import com.nextcloud.utils.LinkHelper;
4243
import com.nextcloud.utils.extensions.ViewExtensionsKt;
4344
import com.nextcloud.utils.mdm.MDMConfig;
4445
import com.owncloud.android.MainApp;
@@ -64,6 +65,7 @@
6465
import com.owncloud.android.lib.resources.shares.OCShare;
6566
import com.owncloud.android.lib.resources.shares.ShareType;
6667
import com.owncloud.android.lib.resources.shares.ShareeUser;
68+
import com.owncloud.android.lib.resources.status.OCCapability;
6769
import com.owncloud.android.lib.resources.tags.Tag;
6870
import com.owncloud.android.operations.RefreshFolderOperation;
6971
import com.owncloud.android.operations.RemoteOperationFailedException;
@@ -77,6 +79,7 @@
7779
import com.owncloud.android.utils.FileSortOrder;
7880
import com.owncloud.android.utils.FileStorageUtils;
7981
import com.owncloud.android.utils.MimeTypeUtil;
82+
import com.owncloud.android.utils.theme.CapabilityUtils;
8083
import com.owncloud.android.utils.theme.ViewThemeUtils;
8184

8285
import java.io.File;
@@ -111,6 +114,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
111114
private final String userId;
112115
private final Activity activity;
113116
private final AppPreferences preferences;
117+
private final OCCapability capability;
114118
private List<OCFile> mFiles = new ArrayList<>();
115119
private final List<OCFile> mFilesAll = new ArrayList<>();
116120
private final boolean hideItemOptions;
@@ -121,7 +125,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
121125
private User user;
122126
private final OCFileListFragmentInterface ocFileListFragmentInterface;
123127

124-
125128
private OCFile currentDirectory;
126129
private static final String TAG = OCFileListAdapter.class.getSimpleName();
127130

@@ -163,6 +166,7 @@ public OCFileListAdapter(
163166
hideItemOptions = argHideItemOptions;
164167
this.gridView = gridView;
165168
mStorageManager = transferServiceGetter.getStorageManager();
169+
this.capability = CapabilityUtils.getCapability(user, activity);
166170

167171
if (activity instanceof FileDisplayActivity) {
168172
((FileDisplayActivity) activity).showSortListGroup(true);
@@ -429,23 +433,45 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
429433
footerViewHolder.getLoadingProgressBar().setVisibility(
430434
ocFileListFragmentInterface.isLoading() ? View.VISIBLE : View.GONE);
431435
} else if (holder instanceof OCFileListHeaderViewHolder headerViewHolder) {
436+
ListHeaderBinding headerBinding = headerViewHolder.getBinding();
437+
headerViewHolder.getHeaderView().setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked());
438+
432439
String text = currentDirectory.getRichWorkspace();
433440
PreviewTextFragment.setText(headerViewHolder.getHeaderText(), text, null, activity, true, true, viewThemeUtils);
434-
headerViewHolder.getHeaderView().setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked());
435441

436-
ViewExtensionsKt.setVisibleIf(headerViewHolder.getBinding().recommendedFilesRecyclerView, shouldShowRecommendedFiles());
437-
ViewExtensionsKt.setVisibleIf(headerViewHolder.getBinding().recommendedFilesTitle, shouldShowRecommendedFiles());
438-
ViewExtensionsKt.setVisibleIf(headerViewHolder.getBinding().allFilesTitle, shouldShowRecommendedFiles());
442+
// hide header text if empty (server returns NBSP)
443+
ViewExtensionsKt.setVisibleIf(headerViewHolder.getHeaderText(), text != null && !text.isBlank() && !" ".equals(text));
444+
445+
ViewExtensionsKt.setVisibleIf(headerBinding.recommendedFilesRecyclerView, shouldShowRecommendedFiles());
446+
ViewExtensionsKt.setVisibleIf(headerBinding.recommendedFilesTitle, shouldShowRecommendedFiles());
447+
ViewExtensionsKt.setVisibleIf(headerBinding.allFilesTitle, shouldShowRecommendedFiles());
439448

440449
if (shouldShowRecommendedFiles()) {
441-
final var recommendedFilesRecyclerView = headerViewHolder.getBinding().recommendedFilesRecyclerView;
450+
final var recommendedFilesRecyclerView = headerBinding.recommendedFilesRecyclerView;
442451

443452
final LinearLayoutManager layoutManager = new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false);
444453
recommendedFilesRecyclerView.setLayoutManager(layoutManager);
445454

446455
final var adapter = new RecommendedFilesAdapter(recommendedFiles, ocFileListDelegate, this, mStorageManager);
447456
recommendedFilesRecyclerView.setAdapter(adapter);
448457
}
458+
459+
ViewExtensionsKt.setVisibleIf(headerBinding.openIn.getRoot(), shouldShowOpenInNotes());
460+
461+
if (shouldShowOpenInNotes()) {
462+
final var listHeaderOpenInBinding = headerBinding.openIn;
463+
464+
viewThemeUtils.files.themeFilledCardView(listHeaderOpenInBinding.infoCard);
465+
466+
listHeaderOpenInBinding.infoText.setText(String.format(activity.getString(R.string.folder_best_viewed_in),
467+
activity.getString(R.string.ecosystem_apps_notes)));
468+
469+
listHeaderOpenInBinding.openInButton.setText(String.format(activity.getString(R.string.open_in_app),
470+
activity.getString(R.string.ecosystem_apps_display_notes)));
471+
472+
listHeaderOpenInBinding.openInButton.setOnClickListener(v -> LinkHelper.INSTANCE.openAppOrStore(LinkHelper.APP_NEXTCLOUD_NOTES, user, activity));
473+
}
474+
449475
} else {
450476
ListViewHolder gridViewHolder = (ListViewHolder) holder;
451477
OCFile file = getItem(position);
@@ -481,6 +507,12 @@ private boolean shouldShowRecommendedFiles() {
481507
return !recommendedFiles.isEmpty() && currentDirectory.isRootDirectory();
482508
}
483509

510+
private boolean shouldShowOpenInNotes() {
511+
String notesFolderPath = capability.getNotesFolderPath();
512+
String currentPath = currentDirectory.getDecryptedRemotePath();
513+
return notesFolderPath != null && currentPath != null && currentPath.startsWith(notesFolderPath);
514+
}
515+
484516
private void checkVisibilityOfFileFeaturesLayout(ListViewHolder holder) {
485517
int fileFeaturesVisibility = View.GONE;
486518
LinearLayout fileFeaturesLayout = holder.getFileFeaturesLayout();
@@ -758,6 +790,10 @@ public boolean shouldShowHeader() {
758790
return true;
759791
}
760792

793+
if (shouldShowOpenInNotes()) {
794+
return true;
795+
}
796+
761797
if (currentDirectory.getRichWorkspace() == null) {
762798
return false;
763799
}

app/src/main/java/com/owncloud/android/utils/theme/FilesSpecificViewThemeUtils.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,23 @@ class FilesSpecificViewThemeUtils @Inject constructor(
220220
}
221221
}
222222

223+
fun themeFilledCardView(cardView: MaterialCardView) {
224+
withScheme(cardView) { scheme ->
225+
val background = cardView.context.getColor(R.color.grey_200)
226+
cardView.backgroundTintList =
227+
ColorStateList(
228+
arrayOf(
229+
intArrayOf(android.R.attr.state_checked),
230+
intArrayOf(-android.R.attr.state_checked)
231+
),
232+
intArrayOf(
233+
scheme.secondaryContainer,
234+
background
235+
)
236+
)
237+
}
238+
}
239+
223240
fun themeAvatarButton(shareImageView: ImageView) {
224241
withScheme(shareImageView.context) { scheme ->
225242
shareImageView.background.setColorFilter(scheme.primary, PorterDuff.Mode.SRC_IN)

0 commit comments

Comments
 (0)