feat: add ota and ui changes

This commit is contained in:
TCFDONMEZ
2026-09-29 14:32:25 +03:00
parent c102625306
commit 511c166436
61 changed files with 895 additions and 2178 deletions
@@ -12,7 +12,6 @@ import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
@@ -21,6 +20,7 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
@@ -44,17 +44,26 @@ public class MainActivity extends AppCompatActivity {
static final String WIFI_AUTO_KEY = "wifi_auto_connect";
private static final String REPOSITORY_URL = "https://github.com/fatihdonmezdev/MG4ABRP";
/** Top-bar tabs, in page order. Parallel to {@link #panes}. */
/** Navigation destinations in page order. Parallel to {@link #panes}. */
private static final int[] TAB_IDS =
{ R.id.tabAbrp, R.id.tabService, R.id.tabVehicle, R.id.tabWindows, R.id.tabWifi,
{ R.id.tabVehicle, R.id.tabAbrp, R.id.tabService, R.id.tabWifi,
R.id.tabLog };
private static final int[] PAGE_TITLES = {
R.string.design_overview_title, R.string.design_abrp_title,
R.string.design_service_title, R.string.design_wifi_title, R.string.design_log_title
};
private static final int[] PAGE_SUBTITLES = {
R.string.design_overview_subtitle, R.string.design_abrp_subtitle,
R.string.design_service_subtitle, R.string.design_wifi_subtitle, R.string.design_log_subtitle
};
// Status colours come from the palette, not from the Material swatches: #4CAF50 and
// #F44336 sit around 4:1 on this background, which disappears behind a sunlit
// reflection. The palette entries are the lightened variants (7:1 and above).
private static final int COLOR_OK = 0xFF8FE6A6;
private static final int COLOR_ERROR = 0xFFFF9A9A;
private static final int COLOR_PENDING = 0xFFC6CFD8;
private static final int COLOR_OK = 0xFFA4DCC1;
private static final int COLOR_ERROR = 0xFFFFB4AB;
private static final int COLOR_PENDING = 0xFFBDC4C6;
private TextInputLayout apiKeyLayout;
private TextInputEditText apiKeyInput;
@@ -62,10 +71,6 @@ public class MainActivity extends AppCompatActivity {
private TextInputEditText tokenInput;
private SwitchMaterial serviceSwitch;
private SwitchMaterial autostartSwitch;
private SwitchMaterial windowCloseSwitch;
private CompoundButton.OnCheckedChangeListener windowCloseListener;
private TextView windowCloseStatus;
private TextView windowCloseLog;
private TextView statusText;
private Button testButton;
private View connectionStatusRow;
@@ -80,10 +85,9 @@ public class MainActivity extends AppCompatActivity {
private View servicePane;
private View logPane;
private View vehiclePane;
private View windowsPane;
private View wifiPane;
/** The six pages, in the order the top bar lists them. Parallel to {@link #TAB_IDS}. */
/** The five pages in navigation order. Parallel to {@link #TAB_IDS}. */
private View[] panes;
/** Wi-Fi page. The helper is the same one the upload service uses on its own tick. */
@@ -117,7 +121,7 @@ public class MainActivity extends AppCompatActivity {
}
/**
* Wires the top bar to the pager: every tab is a page, and every page is a tab.
* Wires the navigation to the pager: every tab is a page, and every page is a tab.
*
* Each page has its own view type, so the pager asks for it once and keeps it: these
* views are the activity's own, held in {@link #panes}, not rows to be recycled.
@@ -140,10 +144,8 @@ public class MainActivity extends AppCompatActivity {
// state, exactly as they did when they were siblings in the layout.
}
});
// All of them stay alive. They are cheap, and the service switch, the call log, the
// window-close status and the vehicle readings are refreshed by the tick whether or
// not their page is on screen — refreshWindowClose in particular dereferences views
// the pager would otherwise be free to detach.
// Keep all pages alive: the service switch, log and vehicle readings are refreshed
// by the tick even when their page is off screen.
pager.setOffscreenPageLimit(panes.length - 1);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override public void onPageSelected(int position) { markCurrentPage(position); }
@@ -159,7 +161,7 @@ public class MainActivity extends AppCompatActivity {
}
/**
* Marks the top-bar tab of the page on screen.
* Marks the destination and labels the page on screen.
*
* Only isSelected is set: fill, text, icon and stroke come from the
* res/color/nav_tab_*.xml selectors applied by the Widget.EV.NavTab style. isSelected
@@ -169,6 +171,8 @@ public class MainActivity extends AppCompatActivity {
for (int i = 0; i < TAB_IDS.length; i++) {
findViewById(TAB_IDS[i]).setSelected(i == position);
}
((TextView) findViewById(R.id.header_title)).setText(PAGE_TITLES[position]);
((TextView) findViewById(R.id.header_subtitle)).setText(PAGE_SUBTITLES[position]);
}
/** Refreshes state + call log while the screen is visible. */
@@ -179,7 +183,6 @@ public class MainActivity extends AppCompatActivity {
refreshCallLog();
refreshVehicle();
refreshWifi();
refreshWindowClose();
uiHandler.postDelayed(this, 2_000L);
}
};
@@ -200,13 +203,14 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
prefs = getSharedPreferences("abrp_prefs", MODE_PRIVATE);
securePrefs = SecurePrefs.get(this);
// The three pages are inflated here, once, and handed to the pager as fixed,
// The pages are inflated here, once, and handed to the pager as fixed,
// non-recycled items. That is what lets every widget below be looked up now and
// held for the life of the activity, the way it was when the panes were siblings
// in activity_main.xml — a recycled page would invalidate these references as
@@ -214,14 +218,12 @@ public class MainActivity extends AppCompatActivity {
abrpPane = inflatePane(R.layout.pane_abrp);
servicePane = inflatePane(R.layout.pane_service);
vehiclePane = inflatePane(R.layout.pane_vehicle);
windowsPane = inflatePane(R.layout.pane_windows);
wifiPane = inflatePane(R.layout.pane_wifi);
logPane = inflatePane(R.layout.pane_log);
panes = new View[] { abrpPane, servicePane, vehiclePane, windowsPane, wifiPane, logPane };
panes = new View[] { vehiclePane, abrpPane, servicePane, wifiPane, logPane };
bindVehiclePane();
bindWifiPane();
bindWindowClose();
apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout);
apiKeyInput = abrpPane.findViewById(R.id.api_key_input);
@@ -327,51 +329,13 @@ public class MainActivity extends AppCompatActivity {
dialog.show();
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
android.util.DisplayMetrics metrics = getResources().getDisplayMetrics();
int width = Math.min((int) (760 * metrics.density),
metrics.widthPixels - (int) (32 * metrics.density));
dialog.getWindow().setLayout(width, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
}
}
private void bindWindowClose() {
windowCloseSwitch = windowsPane.findViewById(R.id.window_close_switch);
windowCloseStatus = windowsPane.findViewById(R.id.window_close_status);
windowCloseLog = windowsPane.findViewById(R.id.window_close_log);
windowCloseListener = (button, enabled) -> {
prefs.edit().putBoolean(WindowCloseService.KEY_ENABLED, enabled).apply();
if (enabled) WindowCloseService.startIfEnabled(this);
else stopService(new Intent(this, WindowCloseService.class));
refreshWindowClose();
};
windowCloseSwitch.setChecked(prefs.getBoolean(WindowCloseService.KEY_ENABLED, false));
windowCloseSwitch.setOnCheckedChangeListener(windowCloseListener);
// A build that cannot reach the vehicle service would accept the toggle and then fail
// every command, which reads as a broken feature rather than an inapplicable one.
if (!WindowCloseService.canWriteToVehicle()) windowCloseSwitch.setEnabled(false);
WindowCloseService.startIfEnabled(this);
refreshWindowClose();
}
private void refreshWindowClose() {
if (windowCloseSwitch == null) return;
boolean enabled = prefs.getBoolean(WindowCloseService.KEY_ENABLED, false);
if (windowCloseSwitch.isChecked() != enabled) {
// Detached first: setChecked re-enters the listener, which would write the pref
// and restart the service on every one of these two-second refreshes.
windowCloseSwitch.setOnCheckedChangeListener(null);
windowCloseSwitch.setChecked(enabled);
windowCloseSwitch.setOnCheckedChangeListener(windowCloseListener);
}
FirmwareInfo.Gen generation = FirmwareInfo.INSTANCE.getGeneration();
if (!WindowCloseService.canWriteToVehicle()) {
windowCloseStatus.setText(R.string.window_close_no_system_uid);
} else if (!enabled) {
windowCloseStatus.setText(R.string.window_close_stopped);
} else if (generation != FirmwareInfo.Gen.UNKNOWN && generation != FirmwareInfo.Gen.SWI69) {
windowCloseStatus.setText(R.string.window_close_unsupported);
} else {
windowCloseStatus.setText(WindowCloseService.statusResource());
}
windowCloseLog.setText(WindowCloseService.eventLog());
}
@Override
protected void onResume() {
super.onResume();
@@ -638,8 +602,8 @@ public class MainActivity extends AppCompatActivity {
if (sec == current.intervalSec) selected = i;
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, labels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
this, R.layout.spinner_value, labels);
adapter.setDropDownViewResource(R.layout.spinner_dropdown);
intervalSpinner.setAdapter(adapter);
intervalSpinner.setSelection(selected);
intervalSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@@ -690,7 +654,7 @@ public class MainActivity extends AppCompatActivity {
* Checks for a newer build, and installs one if it is there.
*
* The button is disabled for the duration rather than guarded by a flag: the whole
* sequence — API call, download, {@code pm install} — runs on a background thread, and
* sequence — API call, download, installer staging — runs on a background thread, and
* a second press would start a second download of the same APK. It comes back on when
* the result lands, whatever that result is.
*