1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-02 06:52:55 +01:00

Fossil HR: allow app management on watch from GB

This commit is contained in:
Daniel Dakhno 2021-02-01 01:57:04 +01:00
parent 1dc5164423
commit 1505c355ce
11 changed files with 237 additions and 4 deletions

View File

@ -585,6 +585,9 @@
<activity <activity
android:name=".devices.qhybrid.FileManagementActivity" android:name=".devices.qhybrid.FileManagementActivity"
android:exported="true" /> android:exported="true" />
<activity
android:name=".devices.qhybrid.AppsManagementActivity"
android:exported="true" />
<activity android:name=".devices.qhybrid.CalibrationActivity" /> <activity android:name=".devices.qhybrid.CalibrationActivity" />
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,137 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.widget.PopupMenuCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class AppsManagementActivity extends AbstractGBActivity {
ListView appsListView;
String[] appNames;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qhybrid_apps_management);
initViews();
refreshInstalledApps();
}
private void toast(String data) {
GB.toast(data, Toast.LENGTH_LONG, GB.INFO);
}
private void refreshInstalledApps() {
try {
GBDevice selected = GBApplication.app().getDeviceManager().getSelectedDevice();
if (selected.getType() != DeviceType.FOSSILQHYBRID || !selected.isConnected() || !selected.getModel().startsWith("DN") || selected.getState() != GBDevice.State.INITIALIZED) {
throw new RuntimeException("Device not connected");
}
String installedAppsJson = selected.getDeviceInfo("INSTALLED_APPS").getDetails();
if (installedAppsJson == null || installedAppsJson.isEmpty()) {
throw new RuntimeException("cant get installed apps");
}
JSONArray apps = new JSONArray(installedAppsJson);
appNames = new String[apps.length()];
for (int i = 0; i < apps.length(); i++) {
appNames[i] = apps.getString(i);
}
appsListView.setAdapter(new AppsListAdapter(this, appNames));
} catch (Exception e) {
toast(e.getMessage());
finish();
return;
}
}
class AppsListAdapter extends ArrayAdapter<String> {
public AppsListAdapter(@NonNull Context context, @NonNull String[] objects) {
super(context, 0, objects);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater.from(getContext()));
convertView = inflater.inflate(R.layout.fossil_hr_row_installed_app, null);
}
TextView nameView = convertView.findViewById(R.id.fossil_hr_row_app_name);
nameView.setText(getItem(position));
return nameView;
}
}
@Override
protected void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(this).unregisterReceiver(deviceUpdateReceiver);
finish();
}
@Override
protected void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(this).registerReceiver(deviceUpdateReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED));
}
BroadcastReceiver deviceUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
refreshInstalledApps();
}
};
private void initViews() {
appsListView = findViewById(R.id.qhybrid_apps_list);
appsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
PopupMenu menu = new PopupMenu(AppsManagementActivity.this, view);
menu.getMenu()
.add("uninstall")
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UNINSTALL_APP);
intent.putExtra("EXTRA_APP_NAME", appNames[position]);
LocalBroadcastManager.getInstance(AppsManagementActivity.this).sendBroadcast(intent);
return true;
}
});
menu.show();
}
});
}
}

View File

@ -85,6 +85,7 @@ public class HRConfigActivity extends AbstractGBActivity implements View.OnClick
findViewById(R.id.qhybrid_action_add).setOnClickListener(this); findViewById(R.id.qhybrid_action_add).setOnClickListener(this);
findViewById(R.id.qhybrid_file_management_trigger).setOnClickListener(this); findViewById(R.id.qhybrid_file_management_trigger).setOnClickListener(this);
findViewById(R.id.qhybrid_apps_management_trigger).setOnClickListener(this);
findViewById(R.id.calibration_trigger).setOnClickListener(this); findViewById(R.id.calibration_trigger).setOnClickListener(this);
sharedPreferences = GBApplication.getPrefs().getPreferences(); sharedPreferences = GBApplication.getPrefs().getPreferences();
@ -422,6 +423,9 @@ public class HRConfigActivity extends AbstractGBActivity implements View.OnClick
} else if(v.getId() == R.id.qhybrid_file_management_trigger) { } else if(v.getId() == R.id.qhybrid_file_management_trigger) {
finish(); finish();
startActivity(new Intent(getApplicationContext(), FileManagementActivity.class)); startActivity(new Intent(getApplicationContext(), FileManagementActivity.class));
} else if(v.getId() == R.id.qhybrid_apps_management_trigger) {
finish();
startActivity(new Intent(getApplicationContext(), AppsManagementActivity.class));
} else if(v.getId() == R.id.calibration_trigger) { } else if(v.getId() == R.id.calibration_trigger) {
finish(); finish();
startActivity(new Intent(getApplicationContext(), CalibrationActivity.class)); startActivity(new Intent(getApplicationContext(), CalibrationActivity.class));

View File

@ -96,6 +96,7 @@ public class QHybridSupport extends QHybridBaseSupport {
public static final String QHYBRID_COMMAND_SEND_MENU_ITEMS = "nodomain.freeyourgadget.gadgetbridge.Q_SEND_MENU_ITEMS"; public static final String QHYBRID_COMMAND_SEND_MENU_ITEMS = "nodomain.freeyourgadget.gadgetbridge.Q_SEND_MENU_ITEMS";
public static final String QHYBRID_COMMAND_SET_WIDGET_CONTENT = "nodomain.freeyourgadget.gadgetbridge.Q_SET_WIDGET_CONTENT"; public static final String QHYBRID_COMMAND_SET_WIDGET_CONTENT = "nodomain.freeyourgadget.gadgetbridge.Q_SET_WIDGET_CONTENT";
public static final String QHYBRID_COMMAND_SET_BACKGROUND_IMAGE = "nodomain.freeyourgadget.gadgetbridge.Q_SET_BACKGROUND_IMAGE"; public static final String QHYBRID_COMMAND_SET_BACKGROUND_IMAGE = "nodomain.freeyourgadget.gadgetbridge.Q_SET_BACKGROUND_IMAGE";
public static final String QHYBRID_COMMAND_UNINSTALL_APP = "nodomain.freeyourgadget.gadgetbridge.Q_UNINSTALL_APP";
public static final String QHYBRID_COMMAND_DOWNLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_DOWNLOAD_FILE"; public static final String QHYBRID_COMMAND_DOWNLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_DOWNLOAD_FILE";
public static final String QHYBRID_COMMAND_UPLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_UPLOAD_FILE"; public static final String QHYBRID_COMMAND_UPLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_UPLOAD_FILE";
@ -160,6 +161,7 @@ public class QHybridSupport extends QHybridBaseSupport {
commandFilter.addAction(QHYBRID_COMMAND_UPDATE_WIDGETS); commandFilter.addAction(QHYBRID_COMMAND_UPDATE_WIDGETS);
commandFilter.addAction(QHYBRID_COMMAND_SEND_MENU_ITEMS); commandFilter.addAction(QHYBRID_COMMAND_SEND_MENU_ITEMS);
commandFilter.addAction(QHYBRID_COMMAND_SET_BACKGROUND_IMAGE); commandFilter.addAction(QHYBRID_COMMAND_SET_BACKGROUND_IMAGE);
commandFilter.addAction(QHYBRID_COMMAND_UNINSTALL_APP);
commandFilter.addAction(QHYBRID_COMMAND_UPLOAD_FILE); commandFilter.addAction(QHYBRID_COMMAND_UPLOAD_FILE);
commandFilter.addAction(QHYBRID_COMMAND_DOWNLOAD_FILE); commandFilter.addAction(QHYBRID_COMMAND_DOWNLOAD_FILE);
commandReceiver = new BroadcastReceiver() { commandReceiver = new BroadcastReceiver() {
@ -286,6 +288,10 @@ public class QHybridSupport extends QHybridBaseSupport {
handleFileUploadIntent(intent); handleFileUploadIntent(intent);
break; break;
} }
case QHYBRID_COMMAND_UNINSTALL_APP:{
watchAdapter.uninstallApp(intent.getStringExtra("EXTRA_APP_NAME"));
break;
}
} }
} }
}; };

View File

@ -153,4 +153,7 @@ public abstract class WatchAdapter {
public void factoryReset() { public void factoryReset() {
} }
public void uninstallApp(String appName) {
}
} }

View File

@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener; import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@ -215,6 +216,19 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
this.initializeAfterAuthentication(success); this.initializeAfterAuthentication(success);
} }
@Override
public void uninstallApp(String appName) {
for(ApplicationInformation appInfo : this.installedApplications){
if(appInfo.getAppName().equals(appName)){
byte handle = appInfo.getFileHandle();
short fullFileHandle = (short)((FileHandle.APP_CODE.getMajorHandle()) << 8 | handle);
queueWrite(new FileDeleteRequest(fullFileHandle));
listApplications();
break;
}
}
}
private void setVibrationStrength() { private void setVibrationStrength() {
Prefs prefs = new Prefs(getDeviceSpecificPreferences()); Prefs prefs = new Prefs(getDeviceSpecificPreferences());
int vibrationStrengh = prefs.getInt(DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE, 2); int vibrationStrengh = prefs.getInt(DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE, 2);
@ -633,6 +647,10 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent);
} }
}); });
if(handle == FileHandle.APP_CODE){
listApplications();
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
resultIntent.putExtra("EXTRA_SUCCESS", false); resultIntent.putExtra("EXTRA_SUCCESS", false);
@ -1159,19 +1177,20 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
// configs.add(new ButtonConfiguration("bottom_double_click", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_DOUBLE, "musicApp"))); // configs.add(new ButtonConfiguration("bottom_double_click", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_DOUBLE, "musicApp")));
// filter out all apps not installed on watch // filter out all apps not installed on watch
ArrayList<ButtonConfiguration> availableConfigs = new ArrayList<>();
outerLoop: for (ButtonConfiguration config : configs){ outerLoop: for (ButtonConfiguration config : configs){
for(ApplicationInformation installedApp : installedApplications){ for(ApplicationInformation installedApp : installedApplications){
if(installedApp.getAppName().equals(config.getAction())){ if(installedApp.getAppName().equals(config.getAction())){
availableConfigs.add(config);
continue outerLoop; continue outerLoop;
} }
} }
configs.remove(config);
} }
queueWrite(new ButtonConfigurationPutRequest( queueWrite(new ButtonConfigurationPutRequest(
menuItems, menuItems,
configs.toArray(new ButtonConfiguration[0]), availableConfigs.toArray(new ButtonConfiguration[0]),
this this
)); ));
} catch (JSONException e) { } catch (JSONException e) {
@ -1242,7 +1261,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
logger.info("got event id " + eventId); logger.info("got event id " + eventId);
try { try {
String jsonString = new String(value, 3, value.length - 3); String jsonString = new String(value, 3, value.length - 3);
logger.info(jsonString); // logger.info(jsonString);
JSONObject requestJson = new JSONObject(jsonString); JSONObject requestJson = new JSONObject(jsonString);
JSONObject request = requestJson.getJSONObject("req"); JSONObject request = requestJson.getJSONObject("req");

View File

@ -15,4 +15,8 @@ public class ApplicationInformation {
public String getAppName() { public String getAppName() {
return appName; return appName;
} }
public byte getFileHandle() {
return fileHandle;
}
} }

View File

@ -2,10 +2,15 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fo
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import org.json.JSONArray;
import org.json.JSONObject;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle;
@ -27,6 +32,7 @@ public class ApplicationsListRequest extends FileLookupAndGetRequest{
int nameLength = buffer.get() - 1; // cutting off null byte int nameLength = buffer.get() - 1; // cutting off null byte
byte[] nameBuffer = new byte[nameLength]; byte[] nameBuffer = new byte[nameLength];
buffer.get(nameBuffer); buffer.get(nameBuffer);
String name = new String(nameBuffer);
buffer.get(); // null byte buffer.get(); // null byte
byte handle = buffer.get(); byte handle = buffer.get();
int hash = buffer.getInt(); int hash = buffer.getInt();
@ -35,13 +41,20 @@ public class ApplicationsListRequest extends FileLookupAndGetRequest{
buffer.get(), buffer.get(), buffer.get(), buffer.get() buffer.get(), buffer.get(), buffer.get(), buffer.get()
); );
applicationInfos.add(new ApplicationInformation( applicationInfos.add(new ApplicationInformation(
new String(nameBuffer), name,
version, version,
hash, hash,
handle handle
)); ));
} }
((FossilHRWatchAdapter) getAdapter()).setInstalledApplications(applicationInfos); ((FossilHRWatchAdapter) getAdapter()).setInstalledApplications(applicationInfos);
GBDevice device = getAdapter().getDeviceSupport().getDevice();
JSONArray array = new JSONArray();
for(ApplicationInformation info : applicationInfos){
array.put(info.getAppName());
}
device.addDeviceInfo(new GenericItem("INSTALLED_APPS", array.toString()));
device.sendDeviceUpdateIntent(getAdapter().getContext());
} }
public void handleFileLookupError(FILE_LOOKUP_ERROR error){ public void handleFileLookupError(FILE_LOOKUP_ERROR error){

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/qhybrid_apps_list" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -124,6 +124,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="File management" /> android:text="File management" />
<Button
android:id="@+id/qhybrid_apps_management_trigger"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Apps management" />
<Button <Button
android:id="@+id/calibration_trigger" android:id="@+id/calibration_trigger"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="25dp"
android:id="@+id/fossil_hr_row_app_name"
android:padding="5dp"
android:layout_alignParentStart="true"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_delete"
android:layout_alignParentEnd="true" />
</RelativeLayout>