Skip to content

Commit 97ea46b

Browse files
authored
Settings view model (#105)
* Fix merge * Fix merge * Refactor frequency tracking * Update frequency setting safety * Events refactor * Refactor * Refactors * Refactors * Fix issue where unified push checkbox has wrong state on client connection errors * Refactors * Fix device state not updating * Comment * Fix * Store device state again * Refactor * Unified push context * Fix starting service without permissions
1 parent 8c99bbe commit 97ea46b

15 files changed

Lines changed: 582 additions & 240 deletions

‎client/app/src/main/java/com/jackpf/locationhistory/client/BeaconService.java‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ public void onFailure(@NonNull Throwable t) {
7272
};
7373

7474
private final SharedPreferences.OnSharedPreferenceChangeListener configChangeListener = (prefs, key) -> {
75-
log.d("Detected config change");
76-
77-
handler.removeCallbacks(beaconTaskRunner);
78-
handler.postDelayed(beaconTaskRunner, 500); // Debounce config changes
75+
if (key != null &&
76+
(key.equals(ConfigRepository.SERVER_HOST_KEY)
77+
|| key.equals(ConfigRepository.SERVER_PORT_KEY)
78+
|| key.equals(ConfigRepository.UPDATE_INTERVAL_KEY))) {
79+
log.d("Detected config change");
80+
scheduleNext(500); // Debounce config changes with 500ms delay
81+
}
7982
};
8083

8184
private void scheduleNext(long delayMillis) {

‎client/app/src/main/java/com/jackpf/locationhistory/client/MainActivity.java‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
import androidx.core.content.ContextCompat;
88

99
import com.google.android.material.color.DynamicColors;
10+
import com.jackpf.locationhistory.client.permissions.AppRequirementsUtil;
11+
import com.jackpf.locationhistory.client.util.Logger;
1012

1113
public class MainActivity extends AppCompatActivity {
14+
private final Logger log = new Logger(this);
15+
1216
@Override
1317
protected void onCreate(Bundle bundle) {
1418
DynamicColors.applyToActivitiesIfAvailable(getApplication());
@@ -21,6 +25,11 @@ protected void onCreate(Bundle bundle) {
2125
protected void onResume() {
2226
super.onResume();
2327

24-
ContextCompat.startForegroundService(this, new Intent(this, BeaconService.class));
28+
if (AppRequirementsUtil.allGranted(this, AppRequirements.getRequirements(this))) {
29+
log.d("Permissions granted, starting service");
30+
ContextCompat.startForegroundService(this, new Intent(this, BeaconService.class));
31+
} else {
32+
log.w("Permissions not yet granted, not starting service");
33+
}
2534
}
2635
}

‎client/app/src/main/java/com/jackpf/locationhistory/client/config/ConfigRepository.java‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ public class ConfigRepository {
1010

1111
private static final String PREFERENCES_KEY = "beacon";
1212

13-
private static final String DEVICE_ID_KEY = "device-id";
14-
private static final String DEVICE_READY_KEY = "device-ready";
15-
private static final String DEVICE_STATUS_KEY = "device-status";
13+
public static final String DEVICE_ID_KEY = "device-id";
14+
public static final String DEVICE_READY_KEY = "device-ready";
15+
public static final String DEVICE_STATUS_KEY = "device-status";
1616

17-
private static final String SERVER_HOST_KEY = "server-host";
18-
private static final String SERVER_PORT_KEY = "server-port";
17+
public static final String SERVER_HOST_KEY = "server-host";
18+
public static final String SERVER_PORT_KEY = "server-port";
1919

20-
private static final String UPDATE_INTERVAL_KEY = "update-interval";
20+
public static final String UPDATE_INTERVAL_KEY = "update-interval";
2121

22-
private static final String LAST_RUN_TIMESTAMP_KEY = "last-run-timestamp";
22+
public static final String LAST_RUN_TIMESTAMP_KEY = "last-run-timestamp";
2323

2424
public ConfigRepository(Context context) {
2525
this.context = context;
@@ -83,12 +83,12 @@ public long getLastRunTimestamp() {
8383
return prefs.getLong(LAST_RUN_TIMESTAMP_KEY, 0L);
8484
}
8585

86-
public int getUpdateIntervalMinutes() {
87-
return prefs.getInt(UPDATE_INTERVAL_KEY, 15);
86+
public long getUpdateIntervalMinutes() {
87+
return prefs.getLong(UPDATE_INTERVAL_KEY, 15L);
8888
}
8989

90-
public void setUpdateIntervalMinutes(int minutes) {
91-
prefs.edit().putInt(UPDATE_INTERVAL_KEY, minutes).apply();
90+
public void setUpdateIntervalMinutes(long minutes) {
91+
prefs.edit().putLong(UPDATE_INTERVAL_KEY, minutes).apply();
9292
}
9393

9494
public void setLastRunTimestamp(long lastRunTime) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.jackpf.locationhistory.client.push;
2+
3+
import android.content.Context;
4+
5+
import androidx.annotation.NonNull;
6+
7+
import org.unifiedpush.android.connector.UnifiedPush;
8+
9+
import java.util.List;
10+
11+
public class UnifiedPushContext {
12+
private final Context context;
13+
14+
public UnifiedPushContext(@NonNull Context context) {
15+
this.context = context.getApplicationContext();
16+
}
17+
18+
public List<String> getDistributors() {
19+
return UnifiedPush.getDistributors(context);
20+
}
21+
22+
public void register(String distributor) {
23+
UnifiedPushService.register(context, distributor);
24+
}
25+
26+
public void unregister() {
27+
UnifiedPushService.unregister(context);
28+
}
29+
}

‎client/app/src/main/java/com/jackpf/locationhistory/client/push/UnifiedPushService.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ public void onNewEndpoint(@NonNull PushEndpoint pushEndpoint, @NonNull String in
104104
if (configRepository != null && beaconClient != null) {
105105
new PushRegistration(getApplicationContext(), configRepository, unifiedPushStorage, beaconClient)
106106
.register(NAME, pushEndpoint.getUrl());
107+
} else {
108+
// Register failed, make sure we keep the old state
109+
unifiedPushStorage.setEnabled(false);
107110
}
108111
}
109112

@@ -144,6 +147,9 @@ public void onUnregistered(@NonNull String instance) {
144147
if (configRepository != null && beaconClient != null) {
145148
new PushRegistration(getApplicationContext(), configRepository, unifiedPushStorage, beaconClient)
146149
.unregister();
150+
} else {
151+
// Un-register failed, make sure we keep the old state
152+
unifiedPushStorage.setEnabled(true);
147153
}
148154
}
149155

‎client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsFragment.java‎

Lines changed: 76 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,28 @@
66
import android.view.ViewGroup;
77

88
import androidx.annotation.NonNull;
9-
import androidx.core.content.ContextCompat;
109
import androidx.fragment.app.Fragment;
11-
import androidx.lifecycle.LiveData;
10+
import androidx.lifecycle.ViewModelProvider;
1211

13-
import com.google.common.util.concurrent.ListenableFuture;
14-
import com.jackpf.locationhistory.PingResponse;
15-
import com.jackpf.locationhistory.client.MainActivity;
12+
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
1613
import com.jackpf.locationhistory.client.R;
17-
import com.jackpf.locationhistory.client.client.BeaconClientFactory;
1814
import com.jackpf.locationhistory.client.client.ssl.SSLPrompt;
19-
import com.jackpf.locationhistory.client.client.ssl.TrustedCertStorage;
20-
import com.jackpf.locationhistory.client.client.ssl.UntrustedCertException;
21-
import com.jackpf.locationhistory.client.client.util.GrpcFutureWrapper;
22-
import com.jackpf.locationhistory.client.config.ConfigRepository;
2315
import com.jackpf.locationhistory.client.databinding.FragmentSettingsBinding;
24-
import com.jackpf.locationhistory.client.grpc.BeaconClient;
2516
import com.jackpf.locationhistory.client.push.Ntfy;
2617
import com.jackpf.locationhistory.client.push.ObservableUnifiedPushState;
27-
import com.jackpf.locationhistory.client.push.UnifiedPushService;
28-
import com.jackpf.locationhistory.client.util.Logger;
2918

30-
import org.unifiedpush.android.connector.UnifiedPush;
31-
32-
import java.io.IOException;
33-
import java.util.Arrays;
3419
import java.util.List;
3520

3621
import javax.annotation.Nullable;
3722

3823
public class SettingsFragment extends Fragment {
24+
3925
private FragmentSettingsBinding binding;
40-
@Nullable
41-
private ConfigRepository configRepository;
26+
private SettingsViewModel viewModel;
27+
4228
@Nullable
4329
private SSLPrompt sslPrompt;
4430

45-
private final Logger log = new Logger(this);
46-
4731
@Nullable
4832
@Override
4933
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -55,22 +39,17 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
5539
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
5640
super.onViewCreated(view, savedInstanceState);
5741

58-
configRepository = new ConfigRepository(requireContext());
59-
sslPrompt = new SSLPrompt(getActivity());
42+
viewModel = new ViewModelProvider(this).get(SettingsViewModel.class);
43+
sslPrompt = new SSLPrompt(requireActivity());
6044

45+
setupInputs();
6146
setupUnifiedPushListener();
47+
observeEvents();
6248
}
6349

6450
@Override
6551
public void onResume() {
6652
super.onResume();
67-
68-
binding.serverHostInput.setText(configRepository.getServerHost());
69-
binding.serverPortInput.setText(Integer.toString(configRepository.getServerPort()));
70-
binding.updateFrequencyInput.setText(Integer.toString(configRepository.getUpdateIntervalMinutes()));
71-
72-
binding.testButton.setOnClickListener(view -> handleTestClick());
73-
binding.saveButton.setOnClickListener(view -> handleSaveClick());
7453
}
7554

7655
@Override
@@ -84,95 +63,80 @@ public void onDestroyView() {
8463
binding = null;
8564
}
8665

87-
private void handleTestClick() {
88-
if (getActivity() == null) {
89-
return;
90-
}
91-
92-
try {
93-
// Create a temp client with current (non-saved) settings
94-
BeaconClient tempClient = BeaconClientFactory.createClient(
95-
new BeaconClientFactory.BeaconClientParams(
96-
binding.serverHostInput.getText().toString(),
97-
Integer.parseInt(binding.serverPortInput.getText().toString()),
98-
false,
99-
3000 // Smaller timeout for pings
100-
),
101-
new TrustedCertStorage(getActivity())
102-
);
103-
104-
ListenableFuture<PingResponse> pingResponse = tempClient.ping(new GrpcFutureWrapper<>(
105-
response -> getActivity().runOnUiThread(() -> {
106-
if (BeaconClient.isPongResponse(response)) {
107-
Toasts.show(getActivity(), R.string.toast_connection_successful);
108-
} else {
109-
Toasts.show(getActivity(), R.string.toast_invalid_response, response.getMessage());
110-
}
111-
}),
112-
e -> {
113-
if (UntrustedCertException.isCauseOf(e)) {
114-
sslPrompt.show(UntrustedCertException.getCauseFrom(e).getFingerprint(), true);
115-
} else {
116-
requireActivity().runOnUiThread(() ->
117-
Toasts.show(getActivity(), R.string.toast_connection_failed, e.getMessage())
118-
);
119-
}
120-
}
121-
));
122-
123-
pingResponse.addListener(tempClient::close, ContextCompat.getMainExecutor(requireContext()));
124-
} catch (NumberFormatException | IOException e) {
125-
Toasts.show(requireContext(), R.string.toast_invalid_settings, e.getMessage());
126-
}
66+
private void setupInputs() {
67+
binding.serverHostInput.setText(viewModel.getConfig().getServerHost());
68+
binding.serverPortInput.setText(String.valueOf(viewModel.getConfig().getServerPort()));
69+
binding.updateFrequencyInput.setText(Long.toString(viewModel.getConfig().getUpdateIntervalMinutes()));
70+
71+
binding.testButton.setOnClickListener(v -> viewModel.testConnection(
72+
binding.serverHostInput.getText().toString(),
73+
binding.serverPortInput.getText().toString()
74+
));
75+
76+
binding.saveButton.setOnClickListener(v -> viewModel.saveSettings(
77+
binding.serverHostInput.getText().toString(),
78+
binding.serverPortInput.getText().toString(),
79+
binding.updateFrequencyInput.getText().toString()
80+
));
12781
}
12882

129-
private void handleSaveClick() {
130-
if (getActivity() instanceof MainActivity) {
131-
try {
132-
configRepository.setServerHost(binding.serverHostInput.getText().toString());
133-
configRepository.setServerPort(Integer.parseInt(binding.serverPortInput.getText().toString()));
134-
configRepository.setUpdateIntervalMinutes(Integer.parseInt(binding.updateFrequencyInput.getText().toString()));
135-
136-
Toasts.show(requireContext(), R.string.toast_saved);
137-
} catch (NumberFormatException e) {
138-
Toasts.show(requireContext(), R.string.toast_invalid_settings, e.getMessage());
139-
}
140-
}
83+
private void setupUnifiedPushListener() {
84+
ObservableUnifiedPushState.getInstance(requireContext())
85+
.observeEnabled()
86+
.observe(getViewLifecycleOwner(), isEnabled -> {
87+
if (binding.pushRegisterSwitch.isChecked() != isEnabled) {
88+
binding.pushRegisterSwitch.setOnCheckedChangeListener(null);
89+
binding.pushRegisterSwitch.setChecked(isEnabled);
90+
}
91+
binding.pushRegisterSwitch.setOnCheckedChangeListener((v, isChecked) ->
92+
viewModel.handleUnifiedPushToggle(isChecked)
93+
);
94+
});
14195
}
14296

143-
private void setupUnifiedPushListener() {
144-
isUnifiedPushEnabled().observe(getViewLifecycleOwner(), isEnabled -> {
145-
// Prevent infinite loops
146-
if (binding.pushRegisterSwitch.isChecked() != isEnabled) {
147-
binding.pushRegisterSwitch.setOnCheckedChangeListener(null);
148-
binding.pushRegisterSwitch.setChecked(isEnabled);
97+
private void observeEvents() {
98+
viewModel.getEvents().observe(getViewLifecycleOwner(), event -> {
99+
if (event == null) return;
100+
101+
switch (event.getType()) {
102+
case TOAST:
103+
SettingsViewEvent.Toast toast = (SettingsViewEvent.Toast) event;
104+
Toasts.show(requireContext(), toast.getMessageResId(), toast.getFormatArgs());
105+
break;
106+
case SSL_PROMPT:
107+
SettingsViewEvent.SslPrompt sslPromptEvent = (SettingsViewEvent.SslPrompt) event;
108+
if (sslPrompt != null) sslPrompt.show(sslPromptEvent.getFingerprint(), true);
109+
break;
110+
case SET_UNIFIED_PUSH_CHECKED:
111+
SettingsViewEvent.SetUnifiedPushChecked checkedEvent = (SettingsViewEvent.SetUnifiedPushChecked) event;
112+
binding.pushRegisterSwitch.setChecked(checkedEvent.isChecked());
113+
break;
114+
case SHOW_DISTRIBUTOR_PICKER:
115+
SettingsViewEvent.ShowDistributorPicker pickerEvent = (SettingsViewEvent.ShowDistributorPicker) event;
116+
showDistributorPicker(pickerEvent.getDistributors());
117+
break;
118+
case PROMPT_NTFY_INSTALL:
119+
Ntfy.promptInstall(requireContext());
120+
binding.pushRegisterSwitch.setChecked(false); // Make sure we un-check
121+
break;
149122
}
150-
binding.pushRegisterSwitch.setOnCheckedChangeListener((v, isChecked) -> handleUnifiedPushCheck(isChecked));
151123
});
152124
}
153125

154-
private LiveData<Boolean> isUnifiedPushEnabled() {
155-
return ObservableUnifiedPushState
156-
.getInstance(requireContext())
157-
.observeEnabled();
158-
}
159-
160-
private void handleUnifiedPushCheck(boolean isChecked) {
161-
if (getActivity() instanceof MainActivity) {
162-
if (isChecked) {
163-
List<String> distributors = UnifiedPush.getDistributors(requireContext());
164-
if (!distributors.isEmpty()) {
165-
log.d("Found distributors: %s", Arrays.toString(distributors.toArray()));
166-
// TODO Select distributor properly from list
167-
UnifiedPushService.register(requireContext(), distributors.get(0));
168-
} else {
169-
log.d("No push distributors");
170-
Ntfy.promptInstall(requireContext());
171-
binding.pushRegisterSwitch.setChecked(false);
172-
}
173-
} else {
174-
UnifiedPushService.unregister(requireContext());
175-
}
176-
}
126+
private void showDistributorPicker(List<String> distributors) {
127+
String[] distributorsArray = distributors.toArray(new String[0]);
128+
129+
new MaterialAlertDialogBuilder(requireContext())
130+
.setTitle(R.string.select_distributor)
131+
.setItems(distributorsArray, (dialog, which) ->
132+
viewModel.registerUnifiedPush(distributorsArray[which])
133+
)
134+
.setNegativeButton(R.string.cancel, (dialog, which) ->
135+
binding.pushRegisterSwitch.setChecked(false)
136+
)
137+
.setOnCancelListener(dialog ->
138+
binding.pushRegisterSwitch.setChecked(false)
139+
)
140+
.show();
177141
}
178142
}

0 commit comments

Comments
 (0)