Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,12 +700,19 @@ are never persisted.
Both [`backup_rules.xml`](app/src/main/res/xml/backup_rules.xml)
and
[`data_extraction_rules.xml`](app/src/main/res/xml/data_extraction_rules.xml)
include the slot directory and the `DP_QUANTUM_COIN_WALLET_APP_PREF.xml`
prefs file, and explicitly **exclude** the
`qc_strongbox_counter_v1.xml` pref file (its HMAC tag is signed
by a non-extractable AndroidKeystore key that does not survive a
device migration; on the new device the counter is heal-forwarded
from the restored slot's `generation` field).
whitelist exactly the slot directory and the
`DP_QUANTUM_COIN_WALLET_APP_PREF.xml` prefs file via
path-specific `<include>` rules. Because the sharedpref include
names a single file, the
`qc_strongbox_counter_v1.xml` pref file is **excluded by
omission** — no explicit `<exclude>` is used (lint's
`FullBackupContent` check rejects `<exclude>` paths that are
not under any `<include>`, and an explicit exclude here would
be a redundant no-op). The counter pref must not travel because
its HMAC tag is signed by a non-extractable AndroidKeystore key
that does not survive a device migration; on the new device the
counter is heal-forwarded from the restored slot's `generation`
field.

The runtime gate in
[`backup/WalletBackupAgent.java`](app/src/main/java/com/quantumcoin/app/backup/WalletBackupAgent.java)
Expand Down
65 changes: 65 additions & 0 deletions app/src/main/java/com/quantumcoin/app/utils/GlobalMethods.java
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,71 @@ public static void OfflineOrExceptionError(Context context, LinearLayout layout,
}
}

/**
* Surfaces a "service unavailable" / "no connection" failure as a
* non-blocking toast instead of the full-body retry overlay
* (R.layout.retry_layout). The overlay used to swallow the entire
* body region of the home / send / transactions screens whenever a
* single REST call failed (including on a transient 5xx during a
* background poll), which (a) hid wallet info that was already on
* screen behind a "Service unavailable" placeholder and (b) looked
* particularly bad on tablets where the placeholder left a huge
* empty body region under the wallet header.
*
* <p>Replacement contract:
* <ul>
* <li>Toast is shown only when {@code userInitiated} is
* {@code true} (an explicit refresh button tap, retry tap,
* network switch, etc.). Background polls / silent first-load
* fetches stay quiet so the user is not pestered.</li>
* <li>Already-loaded UI state (balance number, token list,
* transactions table, etc.) is intentionally NOT cleared by
* the caller before the fetch, so a transient failure leaves
* the previous values visible instead of replacing them with
* a blank "0" / empty table.</li>
* </ul>
*
* <p>The legacy {@link #OfflineOrExceptionError} helper is kept
* around so external callers don't break, but every internal
* surface has been migrated to this method.
*/
public static void NotifyServiceUnavailable(Context context,
boolean isNetworkAvailable,
boolean userInitiated) {
if (!userInitiated) return;
if (context == null) return;
final String message;
if (!isNetworkAvailable) {
message = context.getString(R.string.general_connect_internet);
} else {
message = context.getString(R.string.offline_exception_error_title)
+ ". "
+ context.getString(R.string.offline_exception_error_subtitle);
}
// NOTE: we deliberately do NOT route through ShowToast here.
// The legacy ShowToast helper wraps the system Toast in a
// CountDownTimer that calls Toast.cancel() at 600ms, which
// forces the toast off-screen long before LENGTH_LONG would
// otherwise (~3.5s on most OEMs) -- on a tablet that 600ms
// flash is essentially invisible because the user's eye is
// still on the refresh button when it disappears, especially
// when the toast renders near the bottom-edge of a large
// screen. We dispatch a plain LENGTH_LONG toast on the main
// thread so the message stays up long enough to read.
final Context appCtx = context.getApplicationContext();
Runnable show = new Runnable() {
@Override
public void run() {
Toast.makeText(appCtx, message, Toast.LENGTH_LONG).show();
}
};
if (Looper.myLooper() == Looper.getMainLooper()) {
show.run();
} else {
new android.os.Handler(Looper.getMainLooper()).post(show);
}
}


public static void ShowToast(final Context context, final String message) {
if (context == null) {
Expand Down
160 changes: 129 additions & 31 deletions app/src/main/java/com/quantumcoin/app/view/activities/HomeActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
Expand Down Expand Up @@ -132,6 +136,7 @@ private void resetIdleLockTimer() {
private TextView walletAddressTextView;
private TextView balanceValueTextView;
private ProgressBar progressBar;
private ImageButton refreshImageButton;
private BottomNavigationView bottomNavigationView;

private LinearLayout linerLayoutOffline;
Expand Down Expand Up @@ -198,6 +203,40 @@ public void onCreate(Bundle savedInstanceState) {
TextView titleTextView = (TextView) findViewById(R.id.textView_home_tile);
TextView loadSeedTextView = (TextView) findViewById(R.id.textView_home_load_seed);

// Edge-to-edge (default on Android 15 / targetSdk 35) lets the
// status bar and any display cutout draw on top of our top-aligned
// header. Push the logo + titles below them via a status-bar +
// cutout inset, while leaving the gradient background pinned to
// the top edge so it still extends under the cutout. The same
// top-inset is applied to the network chip (also alignParentTop)
// so it doesn't get clipped on the right edge of a wide cutout.
ViewCompat.setOnApplyWindowInsetsListener(topLinearLayout,
new OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(
View v, WindowInsetsCompat insets) {
Insets bars = insets.getInsets(
WindowInsetsCompat.Type.statusBars()
| WindowInsetsCompat.Type.displayCutout());
v.setPadding(v.getPaddingLeft(), bars.top,
v.getPaddingRight(), v.getPaddingBottom());
return insets;
}
});
ViewCompat.setOnApplyWindowInsetsListener(blockChainNetworkTextView,
new OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(
View v, WindowInsetsCompat insets) {
Insets bars = insets.getInsets(
WindowInsetsCompat.Type.statusBars()
| WindowInsetsCompat.Type.displayCutout());
v.setPadding(v.getPaddingLeft(), bars.top,
v.getPaddingRight(), v.getPaddingBottom());
return insets;
}
});

//loadSeedsThread(loadSeedTextView);

//Center Relative layout & Image Button
Expand All @@ -210,7 +249,7 @@ public void onCreate(Bundle savedInstanceState) {

TextView balanceCoinSymbolTextView = (TextView) findViewById(R.id.textView_home_coin_symbol);
balanceValueTextView = (TextView) findViewById(R.id.textView_home_balance_value);
ImageButton refreshImageButton = (ImageButton) findViewById(R.id.imageButton_home_refresh);
refreshImageButton = (ImageButton) findViewById(R.id.imageButton_home_refresh);
progressBar = (ProgressBar) findViewById(R.id.progress_home_loader);

TextView sendTitleTextView = (TextView) findViewById(R.id.textView_home_send_title);
Expand Down Expand Up @@ -238,7 +277,12 @@ public void onCreate(Bundle savedInstanceState) {
transactionsTitleTextView.setText(jsonViewModel.getTransactionsByLangValues());

walletAddressTextView.setText("");
balanceValueTextView.setText("");
// "-" placeholder (NOT "0") so the user is never shown an
// authoritative-looking zero balance before the first
// balance fetch completes. Matches the XML default in
// home_activity.xml (textView_home_balance_value) and the
// matching default on the Send / Seed-Confirmation screens.
balanceValueTextView.setText("-");
screenViewType(-1);

//Notification permission
Expand Down Expand Up @@ -467,7 +511,9 @@ public void onBackPressed() {
@Override
public void onHomeMainComplete() {
try {
getBalanceByAccount(walletAddress, balanceValueTextView, progressBar);
// Initial load on fragment-attach is NOT user-initiated, so
// a transient failure here stays silent (no toast).
getBalanceByAccount(walletAddress, balanceValueTextView, progressBar, false);
} catch (Exception e) {
GlobalMethods.ExceptionError(getApplicationContext(), TAG, e);
}
Expand Down Expand Up @@ -498,13 +544,19 @@ public void onHomeMainComplete() {
*/
public void performHomeRefresh() {
try {
// A balance refresh should also invalidate the cached token
// list so the user sees fresh balances and any newly arrived
// tokens. Mirrors iOS where the refresh action re-pulls coin
// balance + tokens together.
getBalanceByAccount(walletAddress, balanceValueTextView, progressBar);
GlobalMethods.CURRENT_WALLET_TOKEN_LIST = new java.util.ArrayList<>();
GlobalMethods.CURRENT_WALLET_TOKEN_LIST_ADDRESS = null;
// userInitiated=true: the user explicitly tapped refresh
// (toolbar button or pull-to-refresh) so a failure here
// surfaces a toast.
getBalanceByAccount(walletAddress, balanceValueTextView, progressBar, true);
// Re-pull the token list, but DO NOT pre-clear the cache or
// the in-fragment list before the fetch. Previously the
// cache was nulled and the network-change broadcast caused
// HomeMainFragment to reset the token adapter to empty
// before re-fetching, which meant a transient REST failure
// wiped the user's visible token table for no reason. The
// cache is now repopulated only by a successful response;
// a failed re-fetch leaves the previously displayed list
// intact (matches iOS UIRefreshControl semantics).
com.quantumcoin.app.events.NetworkChangeBroadcaster
.broadcastActiveNetworkChanged(getApplicationContext());
} catch (Exception e) {
Expand Down Expand Up @@ -616,8 +668,11 @@ private void refreshActiveNetworkChrome(boolean reloadBalance) {
if (reloadBalance
&& walletAddress != null && !walletAddress.isEmpty()
&& balanceValueTextView != null) {
balanceValueTextView.setText("");
getBalanceByAccount(walletAddress, balanceValueTextView, progressBar);
// Network switch is a user-initiated refresh: surface
// failures via toast. Don't clear the existing balance
// first so a transient failure leaves the previous
// value visible until the next successful fetch.
getBalanceByAccount(walletAddress, balanceValueTextView, progressBar, true);
}
} catch (Exception e) {
timber.log.Timber.w(e, "refresh active network chrome");
Expand Down Expand Up @@ -1314,20 +1369,60 @@ private boolean shouldShowHomeOfflineOverlay() {
}
}

/**
* Swap the toolbar refresh icon with the in-place spinner. The
* ImageButton and the ProgressBar share the same horizontal slot
* in home_activity.xml; both are sized 40dp and only one is
* visible at a time so the row's layout never shifts. Helper is
* null-safe so the broadcast-driven re-fetch path can call into
* getBalanceByAccount even if the toolbar views haven't been
* resolved yet.
*/
private void setRefreshLoading(boolean loading) {
if (refreshImageButton != null) {
refreshImageButton.setVisibility(loading ? View.GONE : View.VISIBLE);
}
if (progressBar != null) {
progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}

//Get balance task
private void getBalanceByAccount(String address, TextView balanceValueTextView, ProgressBar progressBar) {
/**
* Fetches the native QC balance and updates the on-screen value.
*
* <p>{@code userInitiated} controls how failures surface:
* <ul>
* <li>{@code true} (refresh button, pull-to-refresh, retry tap,
* network switch) — a transient failure / 5xx / offline
* state shows a non-blocking toast.</li>
* <li>{@code false} (initial fragment-attach load) — failures
* are silent so the user isn't toasted just for opening the
* screen.</li>
* </ul>
*
* <p>The full-body retry overlay ({@code linerLayout_home_offline})
* is no longer surfaced from this path. The previous behavior
* replaced the entire body region of the home screen with a
* "Service unavailable" placeholder whenever a single REST call
* failed, which (a) hid wallet info that was already on screen and
* (b) looked particularly broken on tablets where the placeholder
* left a giant empty area below the wallet header card. The
* existing balance value (the user's last successful read) is also
* intentionally preserved instead of being reset to "0" before the
* fetch, so a transient failure does not visually wipe the balance.
*/
private void getBalanceByAccount(String address, TextView balanceValueTextView,
ProgressBar progressBar, boolean userInitiated) {
try {
linerLayoutOffline.setVisibility(View.GONE);

//Internet connection check
if (GlobalMethods.IsNetworkAvailable(getApplicationContext())) {

progressBar.setVisibility(View.VISIBLE);

balanceValueTextView.setText("0");
setRefreshLoading(true);

String[] taskParams = {address};

final boolean userInitiatedFinal = userInitiated;
AccountBalanceRestTask task = new AccountBalanceRestTask(
getApplicationContext(), new AccountBalanceRestTask.TaskListener() {
@Override
Expand All @@ -1338,31 +1433,34 @@ public void onFinished(BalanceResponse balanceResponse) throws ServiceException

balanceValueTextView.setText(quantity);
}
progressBar.setVisibility(View.GONE);
setRefreshLoading(false);
}

@Override
public void onFailure(com.quantumcoin.app.api.read.ApiException e) {
progressBar.setVisibility(View.GONE);
setRefreshLoading(false);

int code = e.getCode();
boolean check = GlobalMethods.ApiExceptionSourceCodeBoolean(code);
if (check == true) {
GlobalMethods.ApiExceptionSourceCodeRoute(getApplicationContext(), code,
getString(R.string.apierror),
TAG + " : AccountBalanceRestTask : " + e.toString());
} else if (shouldShowHomeOfflineOverlay()) {
GlobalMethods.OfflineOrExceptionError(getApplicationContext(),
linerLayoutOffline, imageViewRetry, textViewTitleRetry,
textViewSubTitleRetry, true);
// Specific HTTP status (401/404/406/409/417):
// route to the per-code toast only when the
// user explicitly asked for this fetch.
if (userInitiatedFinal) {
GlobalMethods.ApiExceptionSourceCodeRoute(getApplicationContext(), code,
getString(R.string.apierror),
TAG + " : AccountBalanceRestTask : " + e.toString());
}
} else {
GlobalMethods.NotifyServiceUnavailable(
getApplicationContext(), true, userInitiatedFinal);
}
}
});
task.execute(taskParams);
} else if (shouldShowHomeOfflineOverlay()) {
GlobalMethods.OfflineOrExceptionError(getApplicationContext(),
linerLayoutOffline, imageViewRetry, textViewTitleRetry,
textViewSubTitleRetry, false);
} else {
GlobalMethods.NotifyServiceUnavailable(
getApplicationContext(), false, userInitiated);
}
} catch (Exception e) {
GlobalMethods.ExceptionError(getApplicationContext(), TAG, e);
Expand Down
Loading
Loading