Welcome to mirror list, hosted at ThFree Co, Russian Federation.

PushNotificationViewModel.java « ui « deck « nextcloud « niedermann « it « java « main « src « app - github.com/stefan-niedermann/nextcloud-deck.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 0dd5a82b9062fa3cbe87213b29238b89e217c3ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package it.niedermann.nextcloud.deck.ui;

import android.annotation.SuppressLint;
import android.app.Application;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Optional;

import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.remote.api.IResponseCallback;
import it.niedermann.nextcloud.deck.remote.api.ResponseCallback;
import it.niedermann.nextcloud.deck.repository.SyncRepository;
import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
import it.niedermann.nextcloud.deck.util.ProjectUtil;

public class PushNotificationViewModel extends BaseViewModel {

    // Provided by Files app NotificationJob
    private static final String KEY_SUBJECT = "subject";
    private static final String KEY_MESSAGE = "message";
    private static final String KEY_LINK = "link";
    private static final String KEY_ACCOUNT = "account";
    private static final String KEY_CARD_REMOTE_ID = "objectId";

    private final MutableLiveData<Account> account = new MutableLiveData<>();

    public PushNotificationViewModel(@NonNull Application application) {
        super(application);
    }

    @WorkerThread
    public void getCardInformation(@Nullable Bundle bundle, @NonNull PushNotificationCallback callback) {
        if (bundle == null) {
            callback.onError(new IllegalArgumentException("Bundle is null"));
            return;
        }

        try {
            final long cardRemoteId = extractCardRemoteId(bundle)
                    .orElseThrow(() -> new IllegalArgumentException("Could not extract cardRemoteId"));
            final var account = extractAccount(bundle)
                    .orElseThrow(() -> new IllegalArgumentException("Account not found"));
            this.account.postValue(account);

            final var syncManager = new SyncRepository(getApplication(), account);

            final var card = syncManager.getCardByRemoteIDDirectly(account.getId(), cardRemoteId);

            if (card.isPresent()) {
                syncManager.synchronizeCard(new ResponseCallback<>(account) {
                    @Override
                    public void onResponse(Boolean response) {
                        final var boardLocalId = extractBoardLocalId(syncManager, account.getId(), cardRemoteId);
                        if (boardLocalId.isPresent()) {
                            callback.onResponse(new CardInformation(account, boardLocalId.get(), card.get().getLocalId()));
                        } else {
                            DeckLog.wtf("Card with local ID", card.get().getLocalId(), "and remote ID", card.get().getId(), "is present, but could not find board for it.");
                            publishErrorToCallback("Given localBoardId for cardRemoteId" + cardRemoteId + "is null.", null, callback, bundle);
                        }
                    }

                    @SuppressLint("MissingSuperCall")
                    @Override
                    public void onError(Throwable throwable) {
                        final var boardLocalId = extractBoardLocalId(syncManager, account.getId(), cardRemoteId);
                        if (boardLocalId.isPresent()) {
                            new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(getApplication(), R.string.card_outdated, Toast.LENGTH_LONG).show());
                            callback.onResponse(new CardInformation(account, boardLocalId.get(), card.get().getLocalId()));
                        } else {
                            DeckLog.wtf("Card with local ID", card.get().getLocalId(), "and remote ID", card.get().getId(), "is present, but could not find board for it.");
                            publishErrorToCallback("Given localBoardId for cardRemoteId" + cardRemoteId + "is null.", null, callback, bundle);
                        }
                    }
                }, card.get());
            } else {
                syncManager.synchronize(new ResponseCallback<>(account) {
                    @Override
                    public void onResponse(Boolean response) {
                        final var card = syncManager.getCardByRemoteIDDirectly(account.getId(), cardRemoteId);
                        if (card.isPresent()) {
                            final var boardLocalId = extractBoardLocalId(syncManager, account.getId(), cardRemoteId);
                            if (boardLocalId.isPresent()) {
                                callback.onResponse(new CardInformation(account, boardLocalId.get(), card.get().getLocalId()));
                            } else {
                                DeckLog.wtf("Card with local ID", card.get().getLocalId(), "and remote ID", card.get().getId(), "is present, but could not find board for it.");
                                publishErrorToCallback("Could not find board locally for card with remote ID" + cardRemoteId + "even after full synchronization", null, callback, bundle);
                            }
                        } else {
                            publishErrorToCallback("Could not find card with remote ID" + cardRemoteId + "even after full synchronization", null, callback, bundle);
                        }
                    }

                    @Override
                    @SuppressLint("MissingSuperCall")
                    public void onError(Throwable throwable) {
                        publishErrorToCallback("Could not extract boardRemoteId", null, callback, bundle);
                    }
                });
            }
        } catch (Throwable throwable) {
            publishErrorToCallback("", throwable, callback, bundle);
        }
    }

    /**
     * If a browser fallback is possible, {@link PushNotificationCallback#fallbackToBrowser(Uri)}
     * will be invoked, otherwise {@link PushNotificationCallback#onError(Throwable)}.
     */
    private void publishErrorToCallback(@NonNull String message, @Nullable Throwable cause, @NonNull PushNotificationCallback callback, @NonNull Bundle bundle) {
        final var fallbackUri = extractFallbackUri(bundle);
        if (fallbackUri.isPresent()) {
            callback.fallbackToBrowser(fallbackUri.get());
        } else {
            final var info = "Error while receiving push notification:\n"
                    + message + "\n"
                    + KEY_SUBJECT + ": [" + bundle.getString(KEY_SUBJECT) + "]\n"
                    + KEY_MESSAGE + ": [" + bundle.getString(KEY_MESSAGE) + "]\n"
                    + KEY_LINK + ": [" + bundle.getString(KEY_LINK) + "]\n"
                    + KEY_CARD_REMOTE_ID + ": [" + bundle.getString(KEY_CARD_REMOTE_ID) + "]\n"
                    + KEY_ACCOUNT + ": [" + bundle.getString(KEY_ACCOUNT) + "]";
            callback.onError(cause == null
                    ? new Exception(info)
                    : new Exception(info, cause));
        }
    }

    private Optional<Uri> extractFallbackUri(@NonNull Bundle bundle) {
        final var link = bundle.getString(KEY_LINK, "");
        if (link.trim().length() == 0) {
            DeckLog.warn(KEY_LINK, "is blank");
            return Optional.empty();
        }
        try {
            return Optional.of(Uri.parse(new URL(link).toString()));
        } catch (MalformedURLException e) {
            DeckLog.warn(KEY_LINK, "is not a valid URL");
            final var account = extractAccount(bundle);
            if (account.isPresent()) {
                return account.flatMap(value -> link.startsWith("/")
                        ? Optional.of(Uri.parse(value.getUrl() + link))
                        : Optional.of(Uri.parse(value.getUrl() + "/" + link)));
            } else {
                DeckLog.warn("Could not extract account");
                final var accountName = Optional.ofNullable(bundle.getString(KEY_ACCOUNT));
                //noinspection SimplifyOptionalCallChains
                if (!accountName.isPresent()) {
                    DeckLog.warn(KEY_ACCOUNT, "is empty");
                    return Optional.empty();
                }
                final var parts = accountName.get().split("@");
                if (parts.length != 2) {
                    DeckLog.warn("Could not split host part from given account", KEY_ACCOUNT);
                    return Optional.empty();
                }
                return link.startsWith("/")
                        ? Optional.of(Uri.parse("https://" + parts[1] + link))
                        : Optional.of(Uri.parse("https://" + parts[1] + "/" + link));
            }
        }
    }

    private Optional<Long> extractCardRemoteId(@NonNull Bundle bundle) {
        try {
            final String cardRemoteIdString = bundle.getString(KEY_CARD_REMOTE_ID);
            return Optional.of(Long.parseLong(cardRemoteIdString));
        } catch (NumberFormatException nfe) {
            DeckLog.warn(nfe);
            try {
                final long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(bundle.getString(KEY_LINK));
                return ids.length == 2
                        ? Optional.of(ids[1])
                        : Optional.empty();
            } catch (IllegalArgumentException iae) {
                DeckLog.warn(iae);
                return Optional.empty();
            }
        }
    }

    private Optional<Account> extractAccount(@NonNull Bundle bundle) {
        return Optional.ofNullable(baseRepository.readAccountDirectly(bundle.getString(KEY_ACCOUNT)));
    }

    private Optional<Long> extractBoardLocalId(@NonNull SyncRepository syncRepository, long accountId, long cardRemoteId) {
        return Optional.ofNullable(syncRepository.getBoardLocalIdByAccountAndCardRemoteIdDirectly(accountId, cardRemoteId));
    }

    public Optional<String> extractSubject(@Nullable Bundle bundle) {
        return extractProperty(bundle, KEY_SUBJECT);
    }

    public Optional<String> extractMessage(@Nullable Bundle bundle) {
        return extractProperty(bundle, KEY_MESSAGE);
    }

    private Optional<String> extractProperty(@Nullable Bundle bundle, @NonNull String property) {
        if (bundle == null) {
            return Optional.empty();
        }
        final String val = bundle.getString(property);
        if (val == null) {
            return Optional.empty();
        }
        if (val.trim().isEmpty()) {
            return Optional.empty();
        }
        return Optional.of(val);
    }

    public LiveData<Integer> getAccount() {
        return new ReactiveLiveData<>(this.account)
                .map(Account::getColor)
                .distinctUntilChanged();
    }

    public interface PushNotificationCallback extends IResponseCallback<CardInformation> {
        void fallbackToBrowser(@NonNull Uri uri);
    }

    public static class CardInformation {
        @NonNull
        public final Account account;
        public final long localBoardId;
        public final long localCardId;

        public CardInformation(@NonNull Account account, long localBoardId, long localCardId) {
            this.account = account;
            this.localBoardId = localBoardId;
            this.localCardId = localCardId;
        }
    }
}