Add FileInstallerActivity

This commit is contained in:
José Rebelo
2025-07-20 13:50:59 +01:00
parent be91ec73ef
commit d2612ff8cb
35 changed files with 744 additions and 214 deletions
+1
View File
@@ -212,6 +212,7 @@ android {
buildFeatures {
aidl = true
buildConfig = true
viewBinding = true
}
}
+4
View File
@@ -273,6 +273,10 @@
<activity
android:name=".activities.FwAppInstallerActivity"
android:label="@string/title_activity_fw_app_insaller"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.install.FileInstallerActivity"
android:label="@string/title_activity_file_installer"
android:parentActivityName=".activities.ControlCenterv2"
android:exported="true">
<intent-filter>
@@ -33,6 +33,7 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NavUtils;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -46,11 +47,8 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -171,9 +169,12 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_appinstaller);
GBDevice dev = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null) {
device = dev;
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device == null) {
// This activity does not support auto-discovery - see FileInstallerActivity for that
GB.toast("No device provided to FwAppInstallerActivity", Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
if (savedInstanceState != null) {
details = savedInstanceState.getParcelableArrayList(ITEM_DETAILS);
@@ -207,13 +208,10 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
filter.addAction(GB.ACTION_SET_FINISHED);
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter);
installButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setInstallEnabled(false);
installHandler.onStartInstall(device);
GBApplication.deviceService(device).onInstallApp(uri, Bundle.EMPTY);
}
installButton.setOnClickListener(v -> {
setInstallEnabled(false);
installHandler.onStartInstall(device);
GBApplication.deviceService(device).onInstallApp(uri, Bundle.EMPTY);
});
closeButton.setOnClickListener(v -> finish());
@@ -222,80 +220,32 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
if (uri == null) { // For "share" intent
uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
}
installHandler = findInstallHandlerFor(uri);
installHandler = device.getDeviceCoordinator().findInstallHandler(uri, this);
if (installHandler == null) {
// Should never happen? at this point, we got here by installing to the device
setInfoText(getString(R.string.installer_activity_unable_to_find_handler));
} else {
setInfoText(getString(R.string.installer_activity_wait_while_determining_status));
return;
}
List<GBDevice> selectedDevices = GBApplication.app().getDeviceManager().getSelectedDevices();
if(selectedDevices.size() == 0){
GB.toast(getString(R.string.open_fw_installer_connect_minimum_one_device), Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
if(selectedDevices.size() != 1){
GB.toast(getString(R.string.open_fw_installer_connect_maximum_one_device), Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
device = selectedDevices.get(0);
setInfoText(getString(R.string.installer_activity_wait_while_determining_status));
// needed to get the device
if (device == null || !device.isConnected()) {
if (!device.isConnected()) {
if (mayConnect) {
connect();
} else {
GBApplication.deviceService(device).requestDeviceInfo();
}
} else {
GBApplication.deviceService(device).requestDeviceInfo();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(ITEM_DETAILS, details);
}
private InstallHandler findInstallHandlerFor(Uri uri) {
for (DeviceCoordinator coordinator : getAllCoordinatorsConnectedFirst()) {
InstallHandler handler = coordinator.findInstallHandler(uri, this);
if (handler != null) {
LOG.info("Found install handler {} from {}", handler.getClass(), coordinator.getClass());
return handler;
}
}
return null;
}
private List<DeviceCoordinator> getAllCoordinatorsConnectedFirst() {
DeviceManager deviceManager = ((GBApplication) getApplicationContext()).getDeviceManager();
List<DeviceCoordinator> connectedCoordinators = new ArrayList<>();
List<DeviceCoordinator> allCoordinators = new ArrayList<>(DeviceType.values().length);
for(DeviceType type : DeviceType.values()){
allCoordinators.add(type.getDeviceCoordinator());
}
List<DeviceCoordinator> sortedCoordinators = new ArrayList<>(allCoordinators.size());
List<GBDevice> devices = deviceManager.getSelectedDevices();
for(GBDevice connectedDevice : devices){
if (connectedDevice.isConnected()) {
DeviceCoordinator coordinator = connectedDevice.getDeviceCoordinator();
if (coordinator != null) {
connectedCoordinators.add(coordinator);
}
}
}
sortedCoordinators.addAll(connectedCoordinators);
for (DeviceCoordinator coordinator : allCoordinators) {
if (!connectedCoordinators.contains(coordinator)) {
sortedCoordinators.add(coordinator);
}
}
return sortedCoordinators;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
@@ -320,7 +270,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
@Override
public void setPreview(@Nullable final Bitmap bitmap) {
previewImage.setImageBitmap(bitmap);
if (previewImage == null) {
if (bitmap == null) {
previewImage.setVisibility(View.GONE);
} else {
previewImage.setVisibility(View.VISIBLE);
@@ -19,22 +19,17 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class OpenFwAppInstallerActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(OpenFwAppInstallerActivity.class);
private int READ_REQUEST_CODE = 42;
private static final int READ_REQUEST_CODE = 42;
private TextView label;
private GBDevice device;
@Override
@@ -43,38 +38,21 @@ public class OpenFwAppInstallerActivity extends AbstractGBActivity {
Bundle extras = getIntent().getExtras();
if (extras != null) {
device = extras.getParcelable(GBDevice.EXTRA_DEVICE);
} else {
}
if (device == null) {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
setContentView(R.layout.activity_open_fw_app_installer);
Button pickFileButton = findViewById(R.id.open_fw_installer_pick_button);
TextView label = findViewById(R.id.open_fw_installer_no_device);
label = findViewById(R.id.open_fw_installer_no_device);
label.setText(String.format(getString(R.string.open_fw_installer_select_file), device.getAliasOrName()));
final List<GBDevice> devices = ((GBApplication) getApplicationContext()).getDeviceManager().getSelectedDevices();
switch (devices.size()) {
case 0:
label.setText(R.string.open_fw_installer_connect_minimum_one_device);
pickFileButton.setEnabled(false);
break;
case 1:
label.setText(String.format(getString(R.string.open_fw_installer_select_file), device.getAliasOrName()));
pickFileButton.setEnabled(true);
break;
default:
label.setText(R.string.open_fw_installer_connect_maximum_one_device);
pickFileButton.setEnabled(false);
}
pickFileButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
pickFileButton.setOnClickListener(view -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
});
}
@@ -82,7 +60,17 @@ public class OpenFwAppInstallerActivity extends AbstractGBActivity {
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Intent startIntent = new Intent(OpenFwAppInstallerActivity.this, FwAppInstallerActivity.class);
final InstallHandler installHandler = device.getDeviceCoordinator().findInstallHandler(
resultData.getData(),
GBApplication.app().getApplicationContext()
);
if (installHandler == null) {
label.setText(R.string.fwinstaller_file_not_compatible_to_device);
return;
}
final Intent startIntent = new Intent(OpenFwAppInstallerActivity.this, installHandler.getInstallActivity());
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
startIntent.setAction(Intent.ACTION_VIEW);
startIntent.setDataAndType(resultData.getData(), null);
@@ -19,9 +19,11 @@ package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -37,6 +39,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import androidx.annotation.NonNull;
import androidx.core.app.NavUtils;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -44,16 +47,17 @@ import androidx.viewpager.widget.ViewPager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class AppManagerActivity extends AbstractGBFragmentActivity {
private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class);
private int READ_REQUEST_CODE = 42;
private static final int READ_REQUEST_CODE = 42;
private GBDevice mGBDevice = null;
@@ -128,18 +132,16 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
super(fm);
}
@NonNull
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
switch (enabledTabsList.get(position)) {
case "cache":
return new AppManagerFragmentCache();
case "apps":
return new AppManagerFragmentInstalledApps();
case "watchfaces":
return new AppManagerFragmentInstalledWatchfaces();
}
return null;
return switch (enabledTabsList.get(position)) {
case "cache" -> new AppManagerFragmentCache();
case "apps" -> new AppManagerFragmentInstalledApps();
case "watchfaces" -> new AppManagerFragmentInstalledWatchfaces();
default -> throw new IllegalArgumentException("Unknown tab position " + position);
};
}
@Override
@@ -149,15 +151,12 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
@Override
public CharSequence getPageTitle(int position) {
switch (enabledTabsList.get(position)) {
case "cache":
return getString(R.string.appmanager_cached_watchapps_watchfaces);
case "apps":
return getString(R.string.appmanager_installed_watchapps);
case "watchfaces":
return getString(R.string.appmanager_installed_watchfaces);
}
return super.getPageTitle(position);
return switch (enabledTabsList.get(position)) {
case "cache" -> getString(R.string.appmanager_cached_watchapps_watchfaces);
case "apps" -> getString(R.string.appmanager_installed_watchapps);
case "watchfaces" -> getString(R.string.appmanager_installed_watchfaces);
default -> super.getPageTitle(position);
};
}
}
@@ -208,9 +207,16 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Intent startIntent = new Intent(AppManagerActivity.this, FwAppInstallerActivity.class);
final Uri uri = resultData.getData();
final InstallHandler installHandler = mGBDevice.getDeviceCoordinator().findInstallHandler(uri, this);
if (installHandler == null) {
GB.toast(getString(R.string.fwinstaller_file_not_compatible_to_device), Toast.LENGTH_LONG, GB.INFO);
return;
}
final Intent startIntent = new Intent(AppManagerActivity.this, installHandler.getInstallActivity());
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startIntent.setAction(Intent.ACTION_VIEW);
startIntent.setDataAndType(resultData.getData(), null);
startIntent.setDataAndType(uri, null);
startActivity(startIntent);
}
}
@@ -0,0 +1,148 @@
package nodomain.freeyourgadget.gadgetbridge.activities.install
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceInstallAdapter
import nodomain.freeyourgadget.gadgetbridge.databinding.ActivityFileInstallerBinding
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import org.slf4j.LoggerFactory
class FileInstallerActivity : AppCompatActivity() {
private val viewModel: FileInstallerViewModel by viewModels {
FileInstallerViewModelFactory(GBApplication.app())
}
private lateinit var binding: ActivityFileInstallerBinding
private lateinit var deviceAdapter: DeviceInstallAdapter
private val handlers: MutableMap<GBDevice, InstallHandler> = mutableMapOf()
private var currentUri: Uri? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFileInstallerBinding.inflate(layoutInflater)
setContentView(binding.root)
currentUri = intent.data ?: intent.getParcelableExtra(Intent.EXTRA_STREAM)
// Setup the RecyclerView
deviceAdapter = DeviceInstallAdapter { selectedDevice ->
launchInstallActivity(selectedDevice, handlers[selectedDevice], currentUri)
}
binding.recyclerViewDevices.apply {
adapter = deviceAdapter
layoutManager = LinearLayoutManager(this@FileInstallerActivity)
}
viewModel.findCompatibleDevices(currentUri)
viewModel.uiState.observe(this) { state ->
updateUi(state)
}
}
private fun updateUi(state: InstallDeviceUiState) {
// Hide everything initially, then show the relevant one
binding.fileInstallerLoadingProgress.visibility = View.GONE
binding.recyclerViewDevices.visibility = View.GONE
binding.textInstallerStatus.visibility = View.GONE
when (state) {
is InstallDeviceUiState.Loading -> {
binding.fileInstallerLoadingProgress.visibility = View.VISIBLE
LOG.debug("Loading compatible devices...")
}
is InstallDeviceUiState.Success -> {
if (state.results.isEmpty()) { // Ideally this should be caught by Error state
binding.textInstallerStatus.text = getString(R.string.fwinstaller_no_compatible_device_found, currentUri)
binding.textInstallerStatus.visibility = View.VISIBLE
} else {
binding.recyclerViewDevices.visibility = View.VISIBLE
handlers.clear()
for (pair in state.results) {
handlers.put(pair.first, pair.second)
}
deviceAdapter.submitList(state.results.map { pair -> pair.first })
if (state.results.size == 1) {
// Single device was found - start the install activity directly
val (device, handler) = state.results[0]
launchInstallActivity(device, handler, currentUri)
}
}
}
is InstallDeviceUiState.Error -> {
binding.textInstallerStatus.text = state.message
binding.textInstallerStatus.visibility = View.VISIBLE
LOG.error("Error: ${state.message}")
}
is InstallDeviceUiState.NoUri -> {
// Should never happen
@SuppressLint("SetTextI18n")
binding.textInstallerStatus.text = "No file URI provided to install."
binding.textInstallerStatus.visibility = View.VISIBLE
LOG.warn("No URI provided.")
}
}
}
private fun launchInstallActivity(selectedDevice: GBDevice, installHandler: InstallHandler?, uri: Uri?) {
if (uri == null) {
Toast.makeText(this, "Cannot install without a file URI.", Toast.LENGTH_LONG).show()
finish()
return
}
if (installHandler == null) {
// This should never happen
Toast.makeText(this, "Failed to find installHandler", Toast.LENGTH_LONG).show()
finish()
return
}
LOG.info(
"Launching install activity {} for {}",
installHandler.installActivity.simpleName,
selectedDevice
)
val intent = Intent(this, installHandler.installActivity).apply {
setDataAndType(uri, contentResolver.getType(uri))
putExtra(GBDevice.EXTRA_DEVICE, selectedDevice)
setAction(Intent.ACTION_VIEW)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_TASK_ON_HOME or
Intent.FLAG_GRANT_READ_URI_PERMISSION
}
startActivity(intent)
finish()
}
companion object {
private val LOG = LoggerFactory.getLogger(FileInstallerActivity::class.java)
}
}
// FileInstallerViewModelFactory remains the same
class FileInstallerViewModelFactory(
private val application: GBApplication
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(FileInstallerViewModel::class.java)) {
return FileInstallerViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
@@ -0,0 +1,82 @@
package nodomain.freeyourgadget.gadgetbridge.activities.install
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import org.slf4j.LoggerFactory
sealed class InstallDeviceUiState {
object Loading : InstallDeviceUiState()
data class Success(val results: List<Pair<GBDevice, InstallHandler>>) : InstallDeviceUiState()
object NoUri : InstallDeviceUiState()
data class Error(val message: String) : InstallDeviceUiState()
}
class FileInstallerViewModel(private val application: GBApplication) : ViewModel() {
private val _uiState = MutableLiveData<InstallDeviceUiState>()
val uiState: LiveData<InstallDeviceUiState> = _uiState
fun findCompatibleDevices(uri: Uri?) {
if (uri == null) {
_uiState.value = InstallDeviceUiState.NoUri
return
}
_uiState.value = InstallDeviceUiState.Loading
viewModelScope.launch {
val results = withContext(Dispatchers.IO) {
val compatibleDevices = mutableListOf<Pair<GBDevice, InstallHandler>>()
for (device in getAllDeviceTypesConnectedFirst()) {
val coordinator = device.deviceCoordinator
try {
val handler = coordinator.findInstallHandler(uri, application.applicationContext)
if (handler != null) {
LOG.debug(
"Found compatible install handler {} for {}",
handler.javaClass.simpleName,
device
)
compatibleDevices.add(Pair(device, handler))
}
} catch (e: Exception) {
LOG.error("Error finding install handler for $device", e)
}
}
compatibleDevices
}
if (results.isNotEmpty()) {
_uiState.value = InstallDeviceUiState.Success(results)
} else {
_uiState.value = InstallDeviceUiState.Error(
application.getString(R.string.fwinstaller_no_compatible_device_found, uri)
)
}
}
}
private fun getAllDeviceTypesConnectedFirst(): List<GBDevice> {
return application.deviceManager.devices
.sortedWith { d1, d2 ->
when {
d1.isConnected && !d2.isConnected -> -1
d2.isConnected && !d1.isConnected -> 1
else -> d1.aliasOrName.compareTo(d2.aliasOrName)
}
}
.toList()
}
companion object {
private val LOG = LoggerFactory.getLogger(FileInstallerViewModel::class.java)
}
}
@@ -381,6 +381,7 @@ public class MusicManagerActivity extends AbstractGBActivity {
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Intent startIntent = new Intent(MusicManagerActivity.this, FwAppInstallerActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startIntent.setAction(Intent.ACTION_VIEW);
startIntent.setDataAndType(result.getData().getData(), null);
startActivity(startIntent);
@@ -0,0 +1,54 @@
package nodomain.freeyourgadget.gadgetbridge.adapter
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import nodomain.freeyourgadget.gadgetbridge.databinding.ListItemDeviceInstallBinding
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
class DeviceInstallAdapter(
private val onItemClicked: (GBDevice) -> Unit
) : ListAdapter<GBDevice, DeviceInstallAdapter.DeviceViewHolder>(GBDeviceDiffUtil()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceViewHolder {
val binding = ListItemDeviceInstallBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return DeviceViewHolder(binding)
}
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
inner class DeviceViewHolder(private val binding: ListItemDeviceInstallBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onItemClicked(getItem(position))
}
}
}
fun bind(device: GBDevice) {
binding.textViewDeviceName.text = device.aliasOrName
binding.imageViewDeviceIcon.setImageResource(device.deviceCoordinator.defaultIconResource)
if (!device.isConnected) {
val colorMatrix = ColorMatrix()
colorMatrix.setSaturation(0f)
binding.imageViewDeviceIcon.colorFilter = ColorMatrixColorFilter(colorMatrix)
} else {
binding.imageViewDeviceIcon.colorFilter = null
}
}
}
}
@@ -68,7 +68,6 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
@@ -84,7 +83,6 @@ import com.google.android.material.snackbar.Snackbar;
import com.jaredrummler.android.colorpicker.ColorPickerDialog;
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1569,21 +1567,6 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
shortcutManager.removeDynamicShortcuts(Collections.singletonList(device.getAddress()));
}
private static class GBDeviceDiffUtil extends DiffUtil.ItemCallback<GBDevice> {
@Override
public boolean areItemsTheSame(@NonNull GBDevice oldItem, @NonNull GBDevice newItem) {
return new EqualsBuilder()
.append(oldItem.getAddress(), newItem.getAddress())
.append(oldItem.getName(), newItem.getName())
.isEquals();
}
@Override
public boolean areContentsTheSame(@NonNull GBDevice oldItem, @NonNull GBDevice newItem) {
return EqualsBuilder.reflectionEquals(oldItem, newItem);
}
}
/**
* A generator of stable IDs, given a string, since hashCode can easily have collisions.
*/
@@ -0,0 +1,41 @@
/* Copyright (C) 2025 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import org.apache.commons.lang3.builder.EqualsBuilder;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
class GBDeviceDiffUtil extends DiffUtil.ItemCallback<GBDevice> {
@Override
public boolean areItemsTheSame(@NonNull final GBDevice oldItem,
@NonNull final GBDevice newItem) {
return new EqualsBuilder()
.append(oldItem.getAddress(), newItem.getAddress())
.append(oldItem.getName(), newItem.getName())
.isEquals();
}
@Override
public boolean areContentsTheSame(@NonNull final GBDevice oldItem,
@NonNull final GBDevice newItem) {
return EqualsBuilder.reflectionEquals(oldItem, newItem);
}
}
@@ -16,6 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices;
import android.app.Activity;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -25,6 +29,9 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
*/
public interface InstallHandler {
@NonNull
Class<? extends Activity> getInstallActivity();
/**
* Returns true if this handler is able to install the element.
* #validateInstallation may only be called if this method returned true.
@@ -17,9 +17,13 @@
package nodomain.freeyourgadget.gadgetbridge.devices.atcbleoepl;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -33,6 +37,12 @@ public class ATCBLEOEPLInstallHandler implements InstallHandler {
mUri = uri;
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return true;
@@ -17,25 +17,52 @@
package nodomain.freeyourgadget.gadgetbridge.devices.divoom;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.provider.MediaStore;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class PixooInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(PixooInstallHandler.class);
private final Context mContext;
private final Uri mUri;
private Bitmap incomingBitmap;
public PixooInstallHandler(Uri uri, Context context) {
mContext = context;
mUri = uri;
try {
incomingBitmap = MediaStore.Images.Media.getBitmap(mContext.getContentResolver(), mUri);
} catch (final IOException e) {
LOG.error("Could not decode image", e);
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return true;
return incomingBitmap != null;
}
@Override
@@ -16,9 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,6 +31,7 @@ import java.io.InputStream;
import java.util.Optional;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -109,6 +113,12 @@ public class GarminFitFileInstallHandler implements InstallHandler {
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
// If we got a fitParseException, the file is "valid" (a fit file) for this handler, but corrupted
@@ -134,13 +144,12 @@ public class GarminFitFileInstallHandler implements InstallHandler {
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof GarminCoordinator)) {
if (!(coordinator instanceof GarminCoordinator garminCoordinator)) {
LOG.warn("Coordinator is not a GarminCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
return;
}
final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator;
final boolean fileSupported = parseFitFile(installActivity, garminCoordinator, device);
if (!fileSupported) {
@@ -166,13 +175,9 @@ public class GarminFitFileInstallHandler implements InstallHandler {
return rawBytes;
}
public FitFile getFitFile() {
return fitFile;
}
public FileType.FILETYPE getFileType() {
return fileType;
}
}
private boolean parseFitFile(final InstallActivity installActivity, final GarminCoordinator coordinator, final GBDevice device) {
final String name;
@@ -16,9 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import static nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability.COURSE_DOWNLOAD;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,6 +32,7 @@ import java.io.IOException;
import java.io.InputStream;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -36,8 +42,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GpxRouteF
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import static nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability.COURSE_DOWNLOAD;
public class GarminGpxRouteInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(GarminGpxRouteInstallHandler.class);
@@ -67,6 +71,12 @@ public class GarminGpxRouteInstallHandler implements InstallHandler {
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return gpxRouteFileConverter != null;
@@ -81,14 +91,13 @@ public class GarminGpxRouteInstallHandler implements InstallHandler {
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof GarminCoordinator)) {
if (!(coordinator instanceof GarminCoordinator garminCoordinator)) {
LOG.warn("Coordinator is not a GarminCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
return;
}
final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator;
if (!garminCoordinator.supports(device, COURSE_DOWNLOAD)) {
if (garminCoordinator.supports(device, COURSE_DOWNLOAD)) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
return;
@@ -16,13 +16,17 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -42,6 +46,12 @@ public class GarminPrgFileInstallHandler implements InstallHandler {
this.prgFile = new GarminPrgFile(uri, context);
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return prgFile.isValid();
@@ -57,14 +67,13 @@ public class GarminPrgFileInstallHandler implements InstallHandler {
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof GarminCoordinator)) {
if (!(coordinator instanceof GarminCoordinator garminCoordinator)) {
LOG.warn("Coordinator is not a GarminCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
installActivity.setCloseEnabled(true);
return;
}
final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator;
// FIXME this might not be the correct capability
if (!garminCoordinator.supports(device, GarminCapability.CONNECTIQ_APP_MANAGEMENT)) {
@@ -16,9 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,6 +30,7 @@ import java.io.IOException;
import java.io.InputStream;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -71,6 +75,12 @@ public class ZeppOsAgpsInstallHandler implements InstallHandler {
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return file != null && file.isValid();
@@ -85,13 +95,12 @@ public class ZeppOsAgpsInstallHandler implements InstallHandler {
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof ZeppOsCoordinator)) {
if (!(coordinator instanceof ZeppOsCoordinator zeppOsCoordinator)) {
LOG.warn("Coordinator is not a ZeppOsCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
return;
}
final ZeppOsCoordinator zeppOsCoordinator = (ZeppOsCoordinator) coordinator;
if (!zeppOsCoordinator.supportsAgpsUpdates()) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
@@ -16,9 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,6 +30,7 @@ import java.io.IOException;
import java.io.InputStream;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -71,6 +75,12 @@ public class ZeppOsGpxRouteInstallHandler implements InstallHandler {
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return file != null && file.isValid();
@@ -85,13 +95,12 @@ public class ZeppOsGpxRouteInstallHandler implements InstallHandler {
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof ZeppOsCoordinator)) {
if (!(coordinator instanceof ZeppOsCoordinator zeppOsCoordinator)) {
LOG.warn("Coordinator is not a ZeppOsCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
return;
}
final ZeppOsCoordinator zeppOsCoordinator = (ZeppOsCoordinator) coordinator;
if (!zeppOsCoordinator.supportsGpxUploads(device)) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
@@ -16,13 +16,17 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -42,6 +46,12 @@ public class ZeppOsMapsInstallHandler implements InstallHandler {
file = new ZeppOsMapsFile(uri, context);
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return file.isValid();
@@ -57,14 +67,13 @@ public class ZeppOsMapsInstallHandler implements InstallHandler {
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof ZeppOsCoordinator)) {
if (!(coordinator instanceof ZeppOsCoordinator zeppOsCoordinator)) {
LOG.warn("Coordinator is not a ZeppOsCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
installActivity.setCloseEnabled(true);
return;
}
final ZeppOsCoordinator zeppOsCoordinator = (ZeppOsCoordinator) coordinator;
if (!zeppOsCoordinator.supportsMaps(device) || zeppOsCoordinator.supportsWifiHotspot(device)) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
@@ -16,9 +16,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
@@ -29,6 +31,7 @@ import java.io.IOException;
import java.io.InputStream;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -52,6 +55,12 @@ public class ZeppOsMusicInstallHandler implements InstallHandler {
this.mAudioInfo = MusicUtils.audioInfoFromUri(context, uri);
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return mAudioInfo != null &&
@@ -17,21 +17,25 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiAppManager;
@@ -110,7 +114,7 @@ public class HuaweiInstallHandler implements InstallHandler {
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof HuaweiCoordinatorSupplier)) {
if (!(coordinator instanceof HuaweiCoordinatorSupplier huaweiCoordinatorSupplier)) {
LOG.warn("Coordinator is not a HuaweiCoordinatorSupplier: {}", coordinator.getClass());
installActivity.setInstallEnabled(false);
return;
@@ -157,13 +161,16 @@ public class HuaweiInstallHandler implements InstallHandler {
}
if (helper.isWatchface()) {
final HuaweiCoordinatorSupplier huaweiCoordinatorSupplier = (HuaweiCoordinatorSupplier) coordinator;
HuaweiWatchfaceManager.WatchfaceDescription description = helper.getWatchfaceDescription();
HuaweiWatchfaceManager.Resolution resolution = new HuaweiWatchfaceManager.Resolution();
String deviceScreen = String.format("%d*%d", huaweiCoordinatorSupplier.getHuaweiCoordinator().getHeight(),
huaweiCoordinatorSupplier.getHuaweiCoordinator().getWidth());
String deviceScreen = String.format(
Locale.ROOT,
"%d*%d",
huaweiCoordinatorSupplier.getHuaweiCoordinator().getHeight(),
huaweiCoordinatorSupplier.getHuaweiCoordinator().getWidth()
);
this.valid = resolution.isValid(description.screen, deviceScreen);
installActivity.setInstallEnabled(true);
@@ -245,8 +252,6 @@ public class HuaweiInstallHandler implements InstallHandler {
LOG.debug("Initialized HuaweiInstallHandler: App");
} else if (helper.isMusic()) {
final HuaweiCoordinatorSupplier huaweiCoordinatorSupplier = (HuaweiCoordinatorSupplier) coordinator;
HuaweiMusicUtils.MusicCapabilities capabilities = huaweiCoordinatorSupplier.getHuaweiCoordinator().getExtendedMusicInfoParams();
if (capabilities == null) {
capabilities = huaweiCoordinatorSupplier.getHuaweiCoordinator().getMusicInfoParams();
@@ -294,6 +299,12 @@ public class HuaweiInstallHandler implements InstallHandler {
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return helper.isValid();
@@ -17,6 +17,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
@@ -26,15 +27,16 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType.AGPS_UIHH;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType.WATCHFACE;
import androidx.annotation.NonNull;
public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWInstallHandler.class);
@@ -42,7 +44,6 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
protected final Uri mUri;
protected final Context mContext;
protected AbstractMiBandFWHelper helper;
private String errorMessage;
public AbstractMiBandFWInstallHandler(Uri uri, Context context) {
mUri = uri;
@@ -51,8 +52,7 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
try {
helper = createHelper(uri, context);
} catch (IOException e) {
errorMessage = e.getMessage();
LOG.warn(errorMessage, e);
LOG.error("Failed to open as miband fw", e);
}
}
@@ -151,6 +151,12 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return helper != null;
@@ -17,9 +17,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
@@ -34,6 +37,7 @@ import java.io.InputStream;
import java.io.Writer;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -119,16 +123,11 @@ public class PBWInstallHandler implements InstallHandler {
if (mPBWReader.isLanguage()) {
drawable = R.drawable.ic_languagepack;
} else {
switch (app.getType()) {
case WATCHFACE:
drawable = R.drawable.ic_watchface;
break;
case APP_ACTIVITYTRACKER:
drawable = R.drawable.ic_activitytracker;
break;
default:
drawable = R.drawable.ic_watchapp;
}
drawable = switch (app.getType()) {
case WATCHFACE -> R.drawable.ic_watchface;
case APP_ACTIVITYTRACKER -> R.drawable.ic_activitytracker;
default -> R.drawable.ic_watchapp;
};
}
installItem.setIcon(drawable);
@@ -155,12 +154,13 @@ public class PBWInstallHandler implements InstallHandler {
GBDeviceApp app = mPBWReader.getGBDeviceApp();
try {
destDir = PebbleUtils.getPbwCacheDir();
//noinspection ResultOfMethodCallIgnored
destDir.mkdirs();
FileUtils.copyURItoFile(mContext, mUri, new File(destDir, app.getUUID().toString() + ".pbw"));
AppManagerActivity.addToAppOrderFile("pbwcacheorder.txt", app.getUUID());
} catch (IOException e) {
LOG.error("Installation failed: " + e.getMessage(), e);
LOG.error("Installation failed: ", e);
return;
}
@@ -169,7 +169,7 @@ public class PBWInstallHandler implements InstallHandler {
try {
writer = new BufferedWriter(new FileWriter(outputFile));
} catch (IOException e) {
LOG.error("Failed to open output file: " + e.getMessage(), e);
LOG.error("Failed to open output file: ", e);
return;
}
try {
@@ -183,26 +183,26 @@ public class PBWInstallHandler implements InstallHandler {
writer.close();
} catch (IOException e) {
LOG.error("Failed to write to output file: " + e.getMessage(), e);
LOG.error("Failed to write to output file: ", e);
} catch (JSONException e) {
LOG.error(e.getMessage(), e);
}
InputStream jsConfigFile = mPBWReader.getInputStreamFile("pebble-js-app.js");
if (jsConfigFile != null) {
try {
try (jsConfigFile) {
outputFile = new File(destDir, app.getUUID().toString() + "_config.js");
FileUtils.copyStreamToFile(jsConfigFile, outputFile);
} catch (IOException e) {
LOG.error("Failed to open output file: " + e.getMessage(), e);
} finally {
try {
jsConfigFile.close();
} catch (IOException e) {
}
LOG.error("Failed to open output file: ", e);
}
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
@@ -16,9 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.pinetime;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import org.slf4j.Logger;
@@ -30,13 +33,14 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException;
public class PineTimeInstallHandler implements InstallHandler {
@@ -112,6 +116,12 @@ public class PineTimeInstallHandler implements InstallHandler {
public void onStartInstall(GBDevice device) {
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return dfuPackageManifest != null &&
@@ -17,11 +17,13 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONException;
@@ -37,6 +39,7 @@ import java.io.IOException;
import java.io.Writer;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
@@ -45,7 +48,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -123,10 +125,11 @@ public class FossilHRInstallHandler implements InstallHandler {
try {
app = fossilFile.getGBDeviceApp();
destDir = mCoordinator.getAppCacheDir();
//noinspection ResultOfMethodCallIgnored
destDir.mkdirs();
FileUtils.copyURItoFile(mContext, fossilFile.getUri(), new File(destDir, app.getUUID().toString() + mCoordinator.getAppFileExtension()));
} catch (IOException e) {
LOG.error("Saving app in cache failed: " + e.getMessage(), e);
LOG.error("Saving app in cache failed: ", e);
return false;
}
// write app metadata
@@ -135,7 +138,7 @@ public class FossilHRInstallHandler implements InstallHandler {
try {
writer = new BufferedWriter(new FileWriter(outputFile));
} catch (IOException e) {
LOG.error("Failed to open output file: " + e.getMessage(), e);
LOG.error("Failed to open output file: ", e);
return false;
}
try {
@@ -149,10 +152,10 @@ public class FossilHRInstallHandler implements InstallHandler {
writer.close();
} catch (IOException e) {
LOG.error("Failed to write to output file: " + e.getMessage(), e);
LOG.error("Failed to write to output file: ", e);
return false;
} catch (JSONException e) {
LOG.error("Failed to load or write appKeys JSON: " + e.getMessage(), e);
LOG.error("Failed to load or write appKeys JSON: ", e);
return false;
}
// write watchface background image
@@ -163,7 +166,7 @@ public class FossilHRInstallHandler implements InstallHandler {
backgroundImg.compress(Bitmap.CompressFormat.PNG, 9, fos);
fos.close();
} catch (IOException e) {
LOG.error("Failed to write to output file: " + e.getMessage(), e);
LOG.error("Failed to write to output file: ", e);
return false;
}
}
@@ -175,13 +178,19 @@ public class FossilHRInstallHandler implements InstallHandler {
previewImg.compress(Bitmap.CompressFormat.PNG, 9, fos);
fos.close();
} catch (IOException e) {
LOG.error("Failed to write to output file: " + e.getMessage(), e);
LOG.error("Failed to write to output file: ", e);
return false;
}
}
return true;
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return fossilFile.isValid();
@@ -16,9 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -26,6 +29,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -104,6 +108,12 @@ public class FossilInstallHandler implements InstallHandler {
public void onStartInstall(GBDevice device) {
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return mIsValid;
@@ -1,10 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.devices.thermalprinter;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,8 +44,10 @@ public class ImageFilePrinterHandler implements InstallHandler {
}
public Bitmap getIncomingBitmap() {
return incomingBitmap;
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return SendToPrinterActivity.class;
}
@Override
@@ -52,10 +57,8 @@ public class ImageFilePrinterHandler implements InstallHandler {
@Override
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
installActivity.setPreview(incomingBitmap);
installActivity.setInstallEnabled(true);
}
@Override
@@ -16,11 +16,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -37,6 +41,12 @@ public class XiaomiInstallHandler implements InstallHandler {
this.helper = new XiaomiFWHelper(uri, context);
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return helper.isValid();
@@ -16,10 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.cmfwatchpro;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -36,6 +40,12 @@ public class CmfInstallHandler implements InstallHandler {
this.helper = new CmfFwHelper(uri, context);
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return helper.isValid();
@@ -18,10 +18,13 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType.AGPS_UIHH;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,6 +39,7 @@ import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@@ -59,6 +63,12 @@ public class ZeppOsFwInstallHandler implements InstallHandler {
mHelper = new ZeppOsFwHelper(uri, context, deviceNames, deviceSources);
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return mHelper.isValid();
@@ -149,6 +159,7 @@ public class ZeppOsFwInstallHandler implements InstallHandler {
// write app zip
final File appOutputFile = new File(appCacheDir, app.getUUID().toString() + coordinator.getAppFileExtension());
try {
//noinspection ResultOfMethodCallIgnored
appCacheDir.mkdirs();
FileUtils.copyURItoFile(mContext, mUri, appOutputFile);
} catch (final IOException e) {
@@ -0,0 +1,45 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".activities.install.FileInstallerActivity">
<ProgressBar
android:id="@+id/fileInstallerLoadingProgress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textInstallerStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="?attr/textAppearanceListItem"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/agps_status_error" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewDevices"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/list_item_device_install" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -15,8 +15,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/about_margin"
android:paddingRight="@dimen/about_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.OpenFwAppInstallerActivity">
android:paddingRight="@dimen/about_margin">
<LinearLayout
android:layout_width="match_parent"
@@ -0,0 +1,42 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/imageViewDeviceIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/icon_placeholder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_device_unknown" />
<TextView
android:id="@+id/textViewDeviceName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="?attr/textAppearanceListItem"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageViewDeviceIcon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/devicetype_unknown" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
+3
View File
@@ -137,6 +137,7 @@
<!-- Strings related to CalBlacklist -->
<string name="title_activity_calblacklist">Blacklisted Calendars</string>
<!-- Strings related to FwAppInstaller -->
<string name="title_activity_file_installer">File installer</string>
<string name="title_activity_fw_app_insaller">FW/App installer</string>
<string name="fw_upgrade_notice">You are about to install the %s.</string>
<string name="fw_upgrade_notice_amazfitbip">You are about to install the %s firmware on your Amazfit Bip.\n\nPlease make sure to install the .fw file, then the .res file, and finally the .gps file. Your watch will reboot after installing the .fw file.\n\nNote: You do not have to install .res and .gps if these files are exactly the same as the ones previously installed.\n\nPROCEED AT YOUR OWN RISK!</string>
@@ -1109,6 +1110,8 @@
<string name="pref_title_keep_data_on_device">Keep activity data on device</string>
<string name="miband_fwinstaller_incompatible_version">Incompatible firmware</string>
<string name="fwinstaller_firmware_not_compatible_to_device">This firmware is not compatible with the device</string>
<string name="fwinstaller_file_not_compatible_to_device">The selected file is not compatible with the device</string>
<string name="fwinstaller_no_compatible_device_found">No compatible device found to install %1s</string>
<string name="miband_prefs_reserve_alarm_calendar">Alarms to reserve for upcoming events</string>
<string name="miband_prefs_reserve_reminder_calendar">Reminders to reserve for upcoming events</string>
<string name="prefs_reserve_reminder_calendar_summary">Number of calendar events that will be synchronized</string>