Skip to content

Commit 272cee8

Browse files
Merge pull request #2899 from nextcloud/feature/2837/ecosystemAppsBarInDrawerHeader
Add app ecosystem bar to drawer header
2 parents ac90e45 + 72bd465 commit 272cee8

File tree

12 files changed

+425
-6
lines changed

12 files changed

+425
-6
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<uses-permission android:name="android.permission.READ_CONTACTS" />
1515
<uses-permission android:name="com.owncloud.android.providers.PERMISSION" />
1616
<queries>
17+
<package android:name="com.nextcloud.talk2" />
1718
<package android:name="com.nextcloud.client" />
1819
<package android:name="com.nextcloud.android.beta" />
1920
</queries>

app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,22 @@
2222
import android.accounts.NetworkErrorException;
2323
import android.animation.AnimatorInflater;
2424
import android.app.SearchManager;
25+
import android.content.Context;
2526
import android.content.Intent;
27+
import android.content.res.ColorStateList;
28+
import android.content.res.Resources;
29+
import android.graphics.Color;
30+
import android.graphics.drawable.GradientDrawable;
2631
import android.net.Uri;
2732
import android.os.Bundle;
2833
import android.text.TextUtils;
34+
import android.util.DisplayMetrics;
2935
import android.util.Log;
36+
import android.view.Menu;
3037
import android.view.View;
38+
import android.widget.ImageView;
39+
import android.widget.LinearLayout;
40+
import android.widget.TextView;
3141

3242
import androidx.activity.OnBackPressedCallback;
3343
import androidx.annotation.ColorInt;
@@ -36,6 +46,7 @@
3646
import androidx.appcompat.app.ActionBarDrawerToggle;
3747
import androidx.appcompat.view.ActionMode;
3848
import androidx.appcompat.widget.SearchView;
49+
import androidx.constraintlayout.widget.ConstraintLayout;
3950
import androidx.coordinatorlayout.widget.CoordinatorLayout;
4051
import androidx.core.app.ActivityCompat;
4152
import androidx.core.content.ContextCompat;
@@ -44,6 +55,7 @@
4455
import androidx.core.view.GravityCompat;
4556
import androidx.lifecycle.Observer;
4657
import androidx.lifecycle.ViewModelProvider;
58+
import androidx.preference.PreferenceManager;
4759
import androidx.recyclerview.selection.SelectionTracker;
4860
import androidx.recyclerview.widget.LinearLayoutManager;
4961
import androidx.recyclerview.widget.RecyclerView;
@@ -66,11 +78,14 @@
6678
import com.nextcloud.android.sso.helper.SingleAccountHelper;
6779

6880
import java.net.HttpURLConnection;
81+
import java.util.Arrays;
6982
import java.util.LinkedList;
83+
import java.util.List;
7084
import java.util.concurrent.ExecutorService;
7185
import java.util.concurrent.Executors;
7286
import java.util.stream.Collectors;
7387

88+
import hct.Hct;
7489
import it.niedermann.android.util.ColorUtil;
7590
import it.niedermann.owncloud.notes.LockedActivity;
7691
import it.niedermann.owncloud.notes.NotesApplication;
@@ -108,6 +123,7 @@
108123
import it.niedermann.owncloud.notes.shared.util.CustomAppGlideModule;
109124
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
110125
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
126+
import it.niedermann.owncloud.notes.util.LinkHelper;
111127

112128
public class MainActivity extends LockedActivity implements NoteClickListener, AccountPickerListener, AccountSwitcherListener, CategoryDialogFragment.CategoryDialogListener {
113129

@@ -170,6 +186,8 @@ protected void onCreate(Bundle savedInstanceState) {
170186

171187
setupToolbars();
172188
setupNavigationList();
189+
setupDrawerAppMenu();
190+
setupDrawerAppMenuListener();
173191
setupNotesList();
174192

175193
mainViewModel.getAccountsCount().observe(this, (count) -> {
@@ -346,6 +364,52 @@ public void handleOnBackPressed() {
346364
});
347365
}
348366

367+
private void setupDrawerAppMenu() {
368+
// hide ecosystem apps based user preference or for branded clients
369+
boolean isShowEcosystemApps = PreferenceManager
370+
.getDefaultSharedPreferences(getApplicationContext())
371+
.getBoolean(getString(R.string.pref_key_show_ecosystem_apps), true);
372+
boolean shouldHideTopBanner = getResources().getBoolean(R.bool.is_branded_client) || !isShowEcosystemApps;
373+
374+
if (shouldHideTopBanner) {
375+
binding.drawerEcosystemApps.setVisibility(GONE);
376+
} else {
377+
binding.drawerEcosystemApps.setVisibility(VISIBLE);
378+
}
379+
}
380+
381+
private void setupDrawerAppMenuListener() {
382+
// Add listeners to the ecosystem items to launch the app or app-store
383+
binding.drawerEcosystemFiles.setOnClickListener(v -> LinkHelper.INSTANCE.openAppOrStore(LinkHelper.APP_NEXTCLOUD_FILES, mainViewModel.getCurrentAccount().getValue().getAccountName(), this));
384+
binding.drawerEcosystemTalk.setOnClickListener(v -> LinkHelper.INSTANCE.openAppOrStore(LinkHelper.APP_NEXTCLOUD_TALK, mainViewModel.getCurrentAccount().getValue().getAccountName(), this));
385+
binding.drawerEcosystemMore.setOnClickListener(v -> LinkHelper.INSTANCE.openAppStore("Nextcloud", true, this));
386+
}
387+
388+
private void themeDrawerAppMenu(int color) {
389+
ColorStateList colorStateList = ColorStateList.valueOf(color);
390+
binding.drawerEcosystemFilesIcon.setImageTintList(colorStateList);
391+
((GradientDrawable) binding.drawerEcosystemFilesIcon.getBackground())
392+
.setStroke(convertDpToPixel(1, this), color);
393+
binding.drawerEcosystemFilesText.setTextColor(color);
394+
395+
binding.drawerEcosystemTalkIcon.setImageTintList(colorStateList);
396+
((GradientDrawable) binding.drawerEcosystemTalkIcon.getBackground())
397+
.setStroke(convertDpToPixel(1, this), color);
398+
binding.drawerEcosystemTalkText.setTextColor(color);
399+
400+
binding.drawerEcosystemMoreIcon.setImageTintList(colorStateList);
401+
((GradientDrawable) binding.drawerEcosystemMoreIcon.getBackground())
402+
.setStroke(convertDpToPixel(1, this), color);
403+
binding.drawerEcosystemMoreText.setTextColor(color);
404+
}
405+
406+
public static int convertDpToPixel(float dp, Context context) {
407+
Resources resources = context.getResources();
408+
DisplayMetrics metrics = resources.getDisplayMetrics();
409+
410+
return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
411+
}
412+
349413
private void showAppAccountNotFoundAlertDialog(NextcloudFilesAppAccountNotFoundException e) {
350414
final MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(this)
351415
.setTitle(NextcloudFilesAppAccountNotFoundException.class.getSimpleName())
@@ -632,6 +696,7 @@ public void applyBrand(int color) {
632696
@ColorInt final int headerTextColor = ColorUtil.getForegroundColorForBackgroundColor(color);
633697
binding.appName.setTextColor(headerTextColor);
634698
DrawableCompat.setTint(binding.logo.getDrawable(), headerTextColor);
699+
themeDrawerAppMenu(headerTextColor);
635700

636701
adapter.applyBrand(color);
637702
adapterCategories.applyBrand(color);

app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
4141
private BrandedSwitchPreference backgroundSyncPref;
4242
private BrandedSwitchPreference keepScreenOnPref;
4343
private BrandedSwitchPreference enableDirectEditorPref;
44+
private BrandedSwitchPreference showEcosystemAppBarPref;
4445

4546
@Override
4647
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -50,6 +51,8 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
5051

5152
fontPref = findPreference(getString(R.string.pref_key_font));
5253

54+
showEcosystemAppBarPref = findPreference(getString(R.string.pref_key_show_ecosystem_apps));
55+
5356
gridViewPref = findPreference(getString(R.string.pref_key_gridview));
5457
if (gridViewPref != null) {
5558
gridViewPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
@@ -141,6 +144,7 @@ public void applyBrand(int color) {
141144
lockPref.applyBrand(color);
142145
wifiOnlyPref.applyBrand(color);
143146
gridViewPref.applyBrand(color);
147+
showEcosystemAppBarPref.applyBrand(color);
144148
preventScreenCapturePref.applyBrand(color);
145149
backgroundSyncPref.applyBrand(color);
146150
keepScreenOnPref.applyBrand(color);
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Nextcloud Android Common Library
3+
*
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
package it.niedermann.owncloud.notes.util
9+
10+
import android.content.ActivityNotFoundException
11+
import android.content.Context
12+
import android.content.Intent
13+
import android.net.Uri
14+
import androidx.core.net.toUri
15+
import com.owncloud.android.lib.common.utils.Log_OC
16+
import java.util.Locale
17+
18+
/**
19+
* Helper class for opening Nextcloud apps if present
20+
* or falling back to opening the app store
21+
* in case the app is not yet installed on the device.
22+
*/
23+
object LinkHelper {
24+
const val APP_NEXTCLOUD_FILES = "com.nextcloud.client"
25+
const val APP_NEXTCLOUD_NOTES = "it.niedermann.owncloud.notes"
26+
const val APP_NEXTCLOUD_TALK = "com.nextcloud.talk2"
27+
const val KEY_ACCOUNT: String = "KEY_ACCOUNT"
28+
private const val TAG = "LinkHelper"
29+
30+
/**
31+
* Open specified app and, if not installed redirect to corresponding download.
32+
*
33+
* @param packageName of app to be opened
34+
* @param userHash to pass in intent
35+
*/
36+
fun openAppOrStore(
37+
packageName: String,
38+
userHash: String?,
39+
context: Context,
40+
) {
41+
val intent = context.packageManager.getLaunchIntentForPackage(packageName)
42+
if (intent != null) {
43+
// app installed - open directly
44+
// TODO handle null user?
45+
intent.putExtra(KEY_ACCOUNT, userHash)
46+
context.startActivity(intent)
47+
} else {
48+
// app not found - open market (Google Play Store, F-Droid, etc.)
49+
openAppStore(packageName, false, context)
50+
}
51+
}
52+
53+
/**
54+
* Open app store page of specified app or search for specified string. Will attempt to open browser when no app
55+
* store is available.
56+
*
57+
* @param string packageName or url-encoded search string
58+
* @param search false -> show app corresponding to packageName; true -> open search for string
59+
*/
60+
fun openAppStore(
61+
string: String,
62+
search: Boolean = false,
63+
context: Context,
64+
) {
65+
var suffix = (if (search) "search?q=" else "details?id=") + string
66+
val intent = Intent(Intent.ACTION_VIEW, "market://$suffix".toUri())
67+
try {
68+
context.startActivity(intent)
69+
} catch (activityNotFoundException1: ActivityNotFoundException) {
70+
// all is lost: open google play store web page for app
71+
if (!search) {
72+
suffix = "apps/$suffix"
73+
}
74+
intent.setData("https://play.google.com/store/$suffix".toUri())
75+
context.startActivity(intent)
76+
}
77+
}
78+
79+
// region Validation
80+
private const val HTTP = "http"
81+
private const val HTTPS = "https"
82+
private const val FILE = "file"
83+
private const val CONTENT = "content"
84+
85+
/**
86+
* Validates if a string can be converted to a valid URI
87+
*/
88+
@Suppress("TooGenericExceptionCaught", "ReturnCount")
89+
fun validateAndGetURI(uriString: String?): Uri? {
90+
if (uriString.isNullOrBlank()) {
91+
Log_OC.w(TAG, "Given uriString is null or blank")
92+
return null
93+
}
94+
95+
return try {
96+
val uri = uriString.toUri()
97+
if (uri.scheme == null) {
98+
return null
99+
}
100+
101+
val validSchemes = listOf(HTTP, HTTPS, FILE, CONTENT)
102+
if (uri.scheme in validSchemes) uri else null
103+
} catch (e: Exception) {
104+
Log_OC.e(TAG, "Invalid URI string: $uriString -- $e")
105+
null
106+
}
107+
}
108+
109+
/**
110+
* Validates if a URL string is valid
111+
*/
112+
@Suppress("TooGenericExceptionCaught", "ReturnCount")
113+
fun validateAndGetURL(url: String?): String? {
114+
if (url.isNullOrBlank()) {
115+
Log_OC.w(TAG, "Given url is null or blank")
116+
return null
117+
}
118+
119+
return try {
120+
val uri = url.toUri()
121+
if (uri.scheme == null) {
122+
return null
123+
}
124+
val validSchemes = listOf(HTTP, HTTPS)
125+
if (uri.scheme in validSchemes) url else null
126+
} catch (e: Exception) {
127+
Log_OC.e(TAG, "Invalid URL: $url -- $e")
128+
null
129+
}
130+
}
131+
// endregion
132+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!--
2+
~ Nextcloud Notes - Android Client
3+
~
4+
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
5+
~ SPDX-License-Identifier: Apache-2.0
6+
-->
7+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
8+
android:width="24dp"
9+
android:height="24dp"
10+
android:tint="#757575"
11+
android:viewportWidth="24"
12+
android:viewportHeight="24">
13+
<path
14+
android:fillColor="@android:color/white"
15+
android:pathData="M19,9l1.25,-2.75L23,5l-2.75,-1.25L19,1l-1.25,2.75L15,5l2.75,1.25L19,9zM11.5,9.5L9,4 6.5,9.5 1,12l5.5,2.5L9,20l2.5,-5.5L17,12l-5.5,-2.5zM19,15l-1.25,2.75L15,19l2.75,1.25L19,23l1.25,-2.75L23,19l-2.75,-1.25L19,15z" />
16+
</vector>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!--
2+
~ Nextcloud Notes - Android Client
3+
~
4+
~ SPDX-FileCopyrightText: 2025 Andy Scherzinger <info@andy-scherzinger>
5+
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
6+
~ SPDX-License-Identifier: Apache-2.0
7+
-->
8+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
9+
android:width="32dp"
10+
android:height="32dp"
11+
android:viewportWidth="32"
12+
android:viewportHeight="32">
13+
<path
14+
android:fillColor="#FF000000"
15+
android:fillType="nonZero"
16+
android:pathData="M17.002,13.258L17.002,11.281C17.002,10.903 17.093,10.562 17.274,10.259C17.456,9.956 17.697,9.725 17.998,9.566C18.606,9.253 19.234,9.017 19.882,8.861C20.53,8.704 21.188,8.626 21.856,8.626C22.521,8.626 23.175,8.704 23.82,8.861C24.465,9.017 25.091,9.253 25.7,9.566C26.001,9.725 26.242,9.956 26.423,10.259C26.605,10.562 26.695,10.903 26.695,11.281L26.695,13.258L17.002,13.258ZM27.871,13.258L27.871,11.115C27.871,10.727 27.765,10.303 27.553,9.843C27.341,9.382 27.009,8.99 26.557,8.667C27.138,8.722 27.684,8.826 28.196,8.978C28.707,9.13 29.18,9.326 29.613,9.566C29.999,9.785 30.293,10.029 30.497,10.297C30.7,10.566 30.802,10.838 30.802,11.115L30.802,13.258L27.871,13.258ZM21.842,7.893C21.127,7.893 20.515,7.638 20.006,7.129C19.497,6.62 19.242,6.008 19.242,5.293C19.242,4.578 19.497,3.966 20.006,3.457C20.515,2.948 21.127,2.694 21.842,2.694C22.557,2.694 23.169,2.948 23.678,3.457C24.187,3.966 24.441,4.578 24.441,5.293C24.441,6.008 24.187,6.62 23.678,7.129C23.169,7.638 22.557,7.893 21.842,7.893ZM28.548,5.293C28.548,6.008 28.294,6.62 27.785,7.129C27.276,7.638 26.664,7.893 25.949,7.893C25.81,7.893 25.621,7.877 25.382,7.844C25.142,7.812 24.953,7.773 24.815,7.727C25.082,7.377 25.283,6.995 25.416,6.581C25.55,6.168 25.617,5.738 25.617,5.293C25.617,4.848 25.55,4.419 25.416,4.005C25.283,3.592 25.082,3.21 24.815,2.86C24.999,2.795 25.188,2.751 25.382,2.728C25.575,2.705 25.764,2.694 25.949,2.694C26.664,2.694 27.276,2.948 27.785,3.457C28.294,3.966 28.548,4.578 28.548,5.293Z" />
17+
<path
18+
android:fillColor="#FF000000"
19+
android:fillType="nonZero"
20+
android:pathData="M2.726,29.614C2.253,29.614 1.849,29.446 1.513,29.11C1.176,28.773 1.008,28.369 1.008,27.897L1.008,19.989C1.008,19.517 1.176,19.113 1.513,18.776C1.849,18.44 2.253,18.272 2.726,18.272L13.291,18.272C13.763,18.272 14.167,18.44 14.504,18.776C14.84,19.113 15.008,19.517 15.008,19.989L15.008,27.897C15.008,28.369 14.84,28.773 14.504,29.11C14.167,29.446 13.763,29.614 13.291,29.614L2.726,29.614ZM8.008,24.964L13.291,21.739L13.291,19.989L8.008,23.295L2.726,19.989L2.726,21.739L8.008,24.964Z" />
21+
<path
22+
android:fillColor="#FF000000"
23+
android:fillType="nonZero"
24+
android:pathData="M18.594,25.823C18.086,25.823 17.653,25.642 17.294,25.281C16.934,24.92 16.755,24.485 16.755,23.978C16.755,23.471 16.935,23.037 17.297,22.678C17.658,22.319 18.092,22.139 18.6,22.139C19.107,22.139 19.541,22.32 19.9,22.681C20.259,23.043 20.439,23.477 20.439,23.984C20.439,24.492 20.258,24.925 19.897,25.284C19.535,25.643 19.101,25.823 18.594,25.823ZM24.002,25.823C23.494,25.823 23.061,25.642 22.702,25.281C22.343,24.92 22.163,24.485 22.163,23.978C22.163,23.471 22.344,23.037 22.705,22.678C23.066,22.319 23.501,22.139 24.008,22.139C24.515,22.139 24.949,22.32 25.308,22.681C25.667,23.043 25.847,23.477 25.847,23.984C25.847,24.492 25.666,24.925 25.305,25.284C24.943,25.643 24.509,25.823 24.002,25.823ZM29.41,25.823C28.902,25.823 28.469,25.642 28.11,25.281C27.751,24.92 27.571,24.485 27.571,23.978C27.571,23.471 27.752,23.037 28.113,22.678C28.474,22.319 28.909,22.139 29.416,22.139C29.923,22.139 30.357,22.32 30.716,22.681C31.075,23.043 31.255,23.477 31.255,23.984C31.255,24.492 31.074,24.925 30.713,25.284C30.351,25.643 29.917,25.823 29.41,25.823Z" />
25+
<path
26+
android:fillColor="#FF000000"
27+
android:fillType="nonZero"
28+
android:pathData="M3.142,14.911C2.76,14.911 2.433,14.775 2.161,14.503C1.889,14.231 1.753,13.904 1.753,13.522L1.753,3.799C1.753,3.417 1.889,3.091 2.161,2.819C2.433,2.547 2.76,2.411 3.142,2.411L3.836,2.411L3.836,1.022L5.225,1.022L5.225,2.411L10.781,2.411L10.781,1.022L12.17,1.022L12.17,2.411L12.864,2.411C13.246,2.411 13.573,2.547 13.845,2.819C14.117,3.091 14.253,3.417 14.253,3.799L14.253,13.522C14.253,13.904 14.117,14.231 13.845,14.503C13.573,14.775 13.246,14.911 12.864,14.911L3.142,14.911ZM3.142,13.522L12.864,13.522L12.864,6.577L3.142,6.577L3.142,13.522Z" />
29+
</vector>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!--
2+
~ Nextcloud Notes - Android Client
3+
~
4+
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
5+
~ SPDX-License-Identifier: Apache-2.0
6+
-->
7+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
8+
android:width="24dp"
9+
android:height="24dp"
10+
android:viewportWidth="960"
11+
android:viewportHeight="960">
12+
<path
13+
android:fillColor="#FF757575"
14+
android:pathData="M226,401L304,434Q318,406 333,380Q348,354 366,328L310,317Q310,317 310,317Q310,317 310,317L226,401ZM368,484L482,597Q524,581 572,548Q620,515 662,473Q732,403 771.5,317.5Q811,232 806,160Q734,155 648,194.5Q562,234 492,304Q450,346 417,394Q384,442 368,484ZM546,419Q523,396 523,362.5Q523,329 546,306Q569,283 603,283Q637,283 660,306Q683,329 683,362.5Q683,396 660,419Q637,442 603,442Q569,442 546,419ZM565,740L649,656Q649,656 649,656Q649,656 649,656L638,600Q612,618 586,632.5Q560,647 532,661L565,740ZM878,87Q897,208 854.5,322.5Q812,437 708,541Q708,541 708,541Q708,541 708,541L728,640Q732,660 726,679Q720,698 706,712L538,880L454,683L283,512L86,428L253,260Q267,246 286.5,240Q306,234 326,238L425,258Q425,258 425,258Q425,258 425,258Q529,154 643,111Q757,68 878,87ZM157,639Q192,604 242.5,603.5Q293,603 328,638Q363,673 362.5,723.5Q362,774 327,809Q302,834 243.5,852Q185,870 82,884Q96,781 114,722.5Q132,664 157,639ZM214,695Q204,705 194,731.5Q184,758 180,785Q207,781 233.5,771.5Q260,762 270,752Q282,740 283,723Q284,706 272,694Q260,682 243,682.5Q226,683 214,695Z" />
15+
</vector>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Nextcloud Notes - Android Client
4+
~
5+
~ SPDX-FileCopyrightText: 2025 Nextcloud Gmbh and Nextcloud contributors
6+
~ SPDX-License-Identifier: GPL-3.0-or-later
7+
-->
8+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
9+
android:shape="oval">
10+
<stroke
11+
android:width="1dp"
12+
android:color="@color/fg_contrast" />
13+
</shape>

0 commit comments

Comments
 (0)