Skip to content
Draft
17 changes: 17 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@
</intent-filter>
</activity>

<activity
android:name=".ui.openlink.OpenLinkActivity"
android:label="@string/app_name"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="*" />
<data android:pathPattern="/.*apps/deck/.*" />
</intent-filter>
</activity>

<activity
android:name=".ui.exception.ExceptionActivity"
android:process=":error_activity" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package it.niedermann.nextcloud.deck;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;

import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;

import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO;
import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES;
import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode;
Expand Down Expand Up @@ -36,10 +40,61 @@ public static boolean isDarkTheme(@NonNull Context context) {
return prefs.getBoolean(context.getString(R.string.pref_key_dark_theme), false);
}

// --------
// Branding
// --------

public static boolean isBrandingEnabled(@NonNull Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(R.string.pref_key_branding), true);
}

@ColorInt
public static int readBrandMainColor(@NonNull Context context) {
if (isBrandingEnabled(context)) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
DeckLog.log("--- Read: shared_preference_theme_main");
return sharedPreferences.getInt(context.getString(R.string.shared_preference_theme_main), context.getApplicationContext().getResources().getColor(R.color.primary));
} else {
return context.getResources().getColor(R.color.primary);
}
}

public static void saveBrandColors(@NonNull Context context, @ColorInt int mainColor) {
if (isBrandingEnabled(context) && context instanceof BrandedActivity) {
final BrandedActivity activity = (BrandedActivity) context;
activity.applyBrand(mainColor);
}
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
DeckLog.log("--- Write: shared_preference_theme_main" + " | " + mainColor);
editor.putInt(context.getString(R.string.shared_preference_theme_main), mainColor);
editor.apply();
}

@SuppressLint("ApplySharedPref")
public static void saveBrandColorsSynchronously(@NonNull Context context, @ColorInt int mainColor) {
if (isBrandingEnabled(context) && context instanceof BrandedActivity) {
final BrandedActivity activity = (BrandedActivity) context;
activity.applyBrand(mainColor);
}
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
DeckLog.log("--- Write: shared_preference_theme_main" + " | " + mainColor);
editor.putInt(context.getString(R.string.shared_preference_theme_main), mainColor);
editor.commit();
}

// --------------------------------------
// Current account / board / stack states
// --------------------------------------

@SuppressLint("ApplySharedPref")
public static void saveCurrentAccountIdSynchronously(@NonNull Context context, long accountId) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
DeckLog.log("--- Write: shared_preference_last_account" + " | " + accountId);
editor.putLong(context.getString(R.string.shared_preference_last_account), accountId);
editor.commit();
}

public static void saveCurrentAccountId(@NonNull Context context, long accountId) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
DeckLog.log("--- Write: shared_preference_last_account" + " | " + accountId);
Expand All @@ -54,6 +109,14 @@ public static long readCurrentAccountId(@NonNull Context context) {
return accountId;
}

@SuppressLint("ApplySharedPref")
public static void saveCurrentBoardIdSynchronously(@NonNull Context context, long accountId, long boardId) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
DeckLog.log("--- Write: shared_preference_last_board_for_account_" + accountId + " | " + boardId);
editor.putLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, boardId);
editor.commit();
}

public static void saveCurrentBoardId(@NonNull Context context, long accountId, long boardId) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
DeckLog.log("--- Write: shared_preference_last_board_for_account_" + accountId + " | " + boardId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package it.niedermann.nextcloud.deck.ui.openlink;

import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AppCompatActivity;

import com.nextcloud.android.sso.helper.SingleAccountHelper;

import java.util.ArrayList;
import java.util.List;

import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainActivity;
import it.niedermann.nextcloud.deck.ui.branding.Branded;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;

import static it.niedermann.nextcloud.deck.DeckApplication.saveBrandColorsSynchronously;
import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountIdSynchronously;
import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardIdSynchronously;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.util.UriUtils.parseBoardRemoteId;

public class OpenLinkActivity extends AppCompatActivity implements Branded {

private SyncManager syncManager;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));

syncManager = new SyncManager(this);

final Intent intent = getIntent();
if (intent == null) {
throw new IllegalArgumentException("Could not retrieve intent");
}

final Uri uri = intent.getData();
if (uri == null) {
throw new IllegalArgumentException("Received uri is null");
}

DeckLog.info("uri: " + uri);
try {
long boardRemoteId = parseBoardRemoteId(uri.toString());
@Nullable String userInfo = uri.getUserInfo();
if (userInfo != null) {
DeckLog.info("uri has userinfo: " + userInfo);
observeOnce(syncManager.readAccount(userInfo + '@' + uri.getHost()), this, (account) -> {
if (account == null) {
DeckLog.info("didn't find account");
// Link has set an explicit user, but we don't have this user
// TODO display account picker
openInBrowser();
} else {
DeckLog.info("found matching account: " + account.getUserName());
// TODO set this account as current account and start MainActivity
launchMainActivity(account, boardRemoteId);
}
});
} else {
DeckLog.info("uri does not have userinfo. Looking for accounts on host " + uri.getHost());
observeOnce(syncManager.readAccountsForHostWithReadAccessToBoard(uri.getHost(), boardRemoteId), this, (accounts) -> {
if (accounts.size() == 0) {
DeckLog.info("found no account on host with read access to this board");
openInBrowser();
} else {
DeckLog.info("found " + accounts.size() + " matching account, using first: " + accounts.get(0).getUserName());
launchMainActivity(accounts.get(0), boardRemoteId);
// TODO set this account as current account and start MainActivity
// TODO #2: Display this as a list to allow a choice by the user
}
});
}
} catch (IllegalArgumentException e) {
DeckLog.logError(e);
openInBrowser();
}
}

@Override
public void applyBrand(int mainColor) {

}

@UiThread
private void launchMainActivity(@NonNull Account account, Long boardRemoteId) {
SingleAccountHelper.setCurrentAccount(this, account.getName());
saveCurrentAccountIdSynchronously(this, account.getId());
try {
saveBrandColorsSynchronously(this, account.getColor());
} catch (Throwable t) {
DeckLog.logError(t);
}
syncManager = new SyncManager(this);
observeOnce(syncManager.getBoardByRemoteId(account.getId(), boardRemoteId), this, (board) -> {
saveCurrentBoardIdSynchronously(this, account.getId(), board.getLocalId());
DeckLog.info("starting " + MainActivity.class.getSimpleName() + " with [" + account + ", " + board.getLocalId() + "]");
Intent intent = new Intent(this, MainActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
});
}

private void openInBrowser() {
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(getIntent().getData(), getIntent().getType());
List<ResolveInfo> activities = getPackageManager().queryIntentActivities(intent, 0);
ArrayList<Intent> targetIntents = new ArrayList<Intent>();
String thisPackageName = getApplicationContext().getPackageName();
for (ResolveInfo currentInfo : activities) {
String packageName = currentInfo.activityInfo.packageName;
if (!thisPackageName.equals(packageName)) {
Intent targetIntent = new Intent(android.content.Intent.ACTION_VIEW);
targetIntent.setDataAndType(intent.getData(), intent.getType());
targetIntent.setPackage(intent.getPackage());
targetIntent.setComponent(new ComponentName(packageName, currentInfo.activityInfo.name));
targetIntents.add(targetIntent);
}
}
if (targetIntents.size() > 0) {
Intent chooserIntent = Intent.createChooser(targetIntents.remove(0), getString(R.string.open_in_browser));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[]{}));
startActivity(chooserIntent);
finish();
}
}
}
24 changes: 24 additions & 0 deletions app/src/main/java/it/niedermann/nextcloud/deck/util/UriUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@

import androidx.annotation.NonNull;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import static java.lang.Long.parseLong;

/**
* A helper class for some {@link Uri} operations.
*/
public final class UriUtils {

private static final String boardsSegment = "board";

private UriUtils() {
// utility class -> private constructor
}
Expand Down Expand Up @@ -93,4 +99,22 @@ private static String getDisplayNameFromContentResolver(Uri uri, Context context
}
return displayName;
}

public static long parseBoardRemoteId(@NonNull String uri) throws IllegalArgumentException {
List<String> segments = Arrays.asList(uri.split("/"));
int boardsSegmentIndex = segments.lastIndexOf(boardsSegment);
if (segments.size() <= boardsSegmentIndex) {
throw new IllegalArgumentException("There was no ID following the last " + boardsSegment + " segment.");
}
final long parsedLong;
try {
parsedLong = parseLong(segments.get(boardsSegmentIndex + 1));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("The ID (\"" + segments.get(boardsSegmentIndex + 1) + "\") the last " + boardsSegment + " is not of type " + Long.class.getSimpleName(), e);
}
if (parsedLong < 1) {
throw new IllegalArgumentException("Parsed board remote id must not be less then 1 but was " + parsedLong);
}
return parsedLong;
}
}
28 changes: 28 additions & 0 deletions app/src/test/java/it/niedermann/nextcloud/deck/UriUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package it.niedermann.nextcloud.deck;

import org.junit.Test;

import it.niedermann.nextcloud.deck.util.UriUtils;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

public class UriUtilsTest {

@Test
public void testParseBoardRemoteId() {
assertEquals(45, UriUtils.parseBoardRemoteId("https://example.com/index.php/apps/deck/#/board/45/card/535"));
assertEquals(13, UriUtils.parseBoardRemoteId("https://example.com/index.php/apps/deck/#/board/13/card/535"));
assertEquals(1, UriUtils.parseBoardRemoteId("https://example.com/index.php/apps/deck/#/board/1/card/535"));

final String[] invalidUris = new String[]{
"",
"https://example.com/index.php/apps/deck/#/board/card/535",
"https://example.com/index.php/apps/deck/#/board/blah/card/535",
"https://example.com/index.php/apps/deck/#/board/-1/card/535"
};
for (String invalidUri : invalidUris) {
assertThrows(invalidUri, IllegalArgumentException.class, () -> UriUtils.parseBoardRemoteId(invalidUri));
}
}
}