/* Copyright (C) 2016-2024 Andreas Shimokawa, Arjan Schrijver, Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti, José Rebelo, Konrad Iturbe, TylerWilliamson 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 . */ package nodomain.freeyourgadget.gadgetbridge.activities.appmanager; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_ACTION_DOWNLOADED_FILE; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.PopupMenu; import android.widget.Toast; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.apache.commons.lang3.StringUtils; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity; import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilHRInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridConstants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager; import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; import nodomain.freeyourgadget.gadgetbridge.util.Version; public abstract class AbstractAppManagerFragment extends Fragment { public static final String ACTION_REFRESH_APPLIST = "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist"; private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class); private static final int CHILD_ACTIVITY_WATCHFACE_EDITOR = 0; private ItemTouchHelper appManagementTouchHelper; protected final List appList = new ArrayList<>(); private GBDeviceAppAdapter mGBDeviceAppAdapter; protected GBDevice mGBDevice = null; protected DeviceCoordinator mCoordinator = null; private Class watchfaceDesignerActivity; protected abstract List getSystemAppsInCategory(); protected abstract String getSortFilename(); protected abstract boolean isCacheManager(); protected abstract boolean filterApp(GBDeviceApp gbDeviceApp); public void startDragging(RecyclerView.ViewHolder viewHolder) { appManagementTouchHelper.startDrag(viewHolder); } protected void onChangedAppOrder() { List uuidList = new ArrayList<>(); for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getAppList()) { uuidList.add(gbDeviceApp.getUUID()); } AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuidList); } protected void refreshList() { appList.clear(); ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename()); List systemApps = getSystemAppsInCategory(); boolean needsRewrite = false; for (GBDeviceApp systemApp : systemApps) { if (!uuids.contains(systemApp.getUUID())) { uuids.add(systemApp.getUUID()); needsRewrite = true; } } if (needsRewrite) { AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuids); } appList.addAll(getCachedApps(uuids)); } private void refreshListFromDevice(Intent intent) { final Map cachedAppsMap = getCachedAppsMap(null); appList.clear(); int appCount = intent.getIntExtra("app_count", 0); for (int i = 0; i < appCount; i++) { String appName = intent.getStringExtra("app_name" + i); String appCreator = intent.getStringExtra("app_creator" + i); String appVersion = intent.getStringExtra("app_version" + i); UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i)); GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i, 0)]; Bitmap previewImage = getAppPreviewImage(uuid.toString()); // Fill out information from the cached app if missing final GBDeviceApp cachedApp = cachedAppsMap.get(uuid); if (cachedApp != null) { if (StringUtils.isBlank(appName)) { appName = cachedApp.getName(); } if (StringUtils.isBlank(appCreator)) { appCreator = cachedApp.getCreator(); } } else { if (StringUtils.isBlank(appName)) { // If the app does not have a name, fallback to uuid appName = uuid.toString(); } } GBDeviceApp app = new GBDeviceApp(uuid, appName, appCreator, appVersion, appType, previewImage); app.setOnDevice(true); if (mGBDevice.getType() == DeviceType.FOSSILQHYBRID) { if ((app.getType() == GBDeviceApp.Type.WATCHFACE) && (!QHybridConstants.HYBRIDHR_WATCHFACE_VERSION.equals(appVersion))) { app.setUpToDate(false); } try { if ((app.getType() == GBDeviceApp.Type.APP_GENERIC) && ((new Version(app.getVersion())).smallerThan(new Version(QHybridConstants.KNOWN_WAPP_VERSIONS.get(app.getName()))))) { app.setUpToDate(false); } } catch (IllegalArgumentException e) { LOG.warn("App JSON: " + app.getJSON().toString()); LOG.warn("Couldn't read app version", e); } } if (filterApp(app)) { appList.add(app); } } if (mGBDevice.getType() == DeviceType.FOSSILQHYBRID) { List systemApps = getSystemAppsInCategory(); for (GBDeviceApp systemApp : systemApps) { appList.add(systemApp); } } } private Bitmap getAppPreviewImage(String name) { Bitmap previewImage = null; try { File cacheDir = mCoordinator.getAppCacheDir(); File previewImgFile = new File(cacheDir, name + "_preview.png"); if (previewImgFile.exists()) { previewImage = BitmapFactory.decodeFile(previewImgFile.getAbsolutePath()); } } catch (IOException e) { LOG.warn("Couldn't load watch app preview image", e); } return previewImage; } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); switch (action) { case ACTION_REFRESH_APPLIST: { if (intent.hasExtra("app_count")) { LOG.info("got app info from device"); if (!isCacheManager()) { LOG.info("will refresh list based on data from device"); refreshListFromDevice(intent); } } else if (mCoordinator.supportsAppListFetching()) { refreshList(); } else if (isCacheManager()) { refreshList(); } mGBDeviceAppAdapter.notifyDataSetChanged(); break; } case QHYBRID_ACTION_DOWNLOADED_FILE: { if (!intent.getBooleanExtra("EXTRA_SUCCESS", false)) { LOG.warn("wapp download was not successful"); GB.toast(context.getString(R.string.appmanager_download_app_error), Toast.LENGTH_LONG, GB.ERROR); break; } if (!intent.getBooleanExtra("EXTRA_TOCACHE", false)) { break; } String path = intent.getStringExtra("EXTRA_PATH"); String name = intent.getStringExtra("EXTRA_NAME"); LOG.info("Attempting to add downloaded app " + name + " to cache"); FossilFileReader fileReader; try { fileReader = new FossilFileReader(Uri.fromFile(new File(path)), context); } catch (IOException e) { LOG.warn("Could not find downloaded wapp", e); break; } if (FossilHRInstallHandler.saveAppInCache(fileReader, fileReader.getBackground(), fileReader.getPreview(), mCoordinator, context)) { LOG.info("Successfully moved downloaded app " + name + " to cache"); GB.toast(String.format(context.getString(R.string.appmanager_downloaded_to_cache), name), Toast.LENGTH_LONG, GB.INFO); if (isCacheManager()) { refreshList(); mGBDeviceAppAdapter.notifyDataSetChanged(); } (new File(path)).delete(); } else { LOG.warn("Parsing downloaded wapp was not successful"); GB.toast(context.getString(R.string.appmanager_download_app_error), Toast.LENGTH_LONG, GB.ERROR); } break; } } } }; protected Map getCachedAppsMap(final List uuids) { final List cachedApps = getCachedApps(uuids); final Map cachedAppsMap = new HashMap<>(); for (GBDeviceApp cachedApp : cachedApps) { cachedAppsMap.put(cachedApp.getUUID(), cachedApp); } return cachedAppsMap; } protected List getCachedApps(List uuids) { List cachedAppList = new ArrayList<>(); File cachePath; try { cachePath = mCoordinator.getAppCacheDir(); } catch (IOException e) { LOG.warn("could not get external dir while reading app cache."); return cachedAppList; } if (cachePath == null) { LOG.warn("Cached apps path is null"); return Collections.emptyList(); } File[] files; if (uuids == null) { files = cachePath.listFiles(); } else { files = new File[uuids.size()]; int index = 0; for (UUID uuid : uuids) { files[index++] = new File(uuid.toString() + mCoordinator.getAppFileExtension()); } } if (files != null) { for (File file : files) { if (file.getName().endsWith(mCoordinator.getAppFileExtension())) { String baseName = file.getName().substring(0, file.getName().length() - mCoordinator.getAppFileExtension().length()); //metadata File jsonFile = new File(cachePath, baseName + ".json"); //configuration File configFile = new File(cachePath, baseName + "_config.js"); try { String jsonstring = FileUtils.getStringFromFile(jsonFile); JSONObject json = new JSONObject(jsonstring); GBDeviceApp app = new GBDeviceApp(json, configFile.exists(), getAppPreviewImage(baseName)); if (mGBDevice.getType() == DeviceType.FOSSILQHYBRID) { if ((app.getType() == GBDeviceApp.Type.WATCHFACE) && (!QHybridConstants.HYBRIDHR_WATCHFACE_VERSION.equals(app.getVersion()))) { app.setUpToDate(false); } try { if ((app.getType() == GBDeviceApp.Type.APP_GENERIC) && ((new Version(app.getVersion())).smallerThan(new Version(QHybridConstants.KNOWN_WAPP_VERSIONS.get(app.getName()))))) { app.setUpToDate(false); } } catch (IllegalArgumentException e) { LOG.warn("Couldn't read app version", e); } } cachedAppList.add(app); } catch (Exception e) { LOG.info("could not read json file for " + baseName); if (mGBDevice.getType() == DeviceType.PEBBLE) { //FIXME: this is really ugly, if we do not find system uuids in pbw cache add them manually. Also duplicated code switch (baseName) { case "8f3c8686-31a1-4f5f-91f5-01600c9bdc59": cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM)); break; case "1f03293d-47af-4f28-b960-f2b02a6dd757": cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); break; case "b2cae818-10f8-46df-ad2b-98ad2254a3c1": cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); break; case "67a32d95-ef69-46d4-a0b9-854cc62f97f9": cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); break; case "18e443ce-38fd-47c8-84d5-6d0c775fbe55": cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); break; case "0863fc6a-66c5-4f62-ab8a-82ed00a98b5d": cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); break; } /* else if (baseName.equals("4dab81a6-d2fc-458a-992c-7a1f3b96a970")) { cachedAppList.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); } else if (baseName.equals("cf1e816a-9db0-4511-bbb8-f60c48ca8fac")) { cachedAppList.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); } */ if (mGBDevice != null) { if (PebbleUtils.hasHealth(mGBDevice.getModel())) { if (baseName.equals(PebbleProtocol.UUID_PEBBLE_HEALTH.toString())) { cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); continue; } } if (PebbleUtils.hasHRM(mGBDevice.getModel())) { if (baseName.equals(PebbleProtocol.UUID_WORKOUT.toString())) { cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); continue; } } if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) { if (baseName.equals("3af858c3-16cb-4561-91e7-f1ad2df8725f")) { cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM)); } if (baseName.equals(PebbleProtocol.UUID_WEATHER.toString())) { cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WEATHER, "Weather (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); } } } if (uuids == null) { cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN)); } } } } } } return cachedAppList; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_REFRESH_APPLIST); filter.addAction(QHYBRID_ACTION_DOWNLOADED_FILE); LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver, filter); if (mCoordinator.supportsAppListFetching()) { GBApplication.deviceService(mGBDevice).onAppInfoReq(); if (isCacheManager()) { refreshList(); } } else { refreshList(); } try { File appCacheDir = mCoordinator.getAppCacheDir(); File appTempDir = new File(appCacheDir, "temp_sharing"); if (appTempDir.isDirectory()) { for (File child : appTempDir.listFiles()) child.delete(); appTempDir.delete(); } } catch (IOException e) { LOG.warn("Could not delete temporary app cache directory", e); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice(); mCoordinator = mGBDevice.getDeviceCoordinator(); final FloatingActionButton appListFab = ((FloatingActionButton) getActivity().findViewById(R.id.fab)); final FloatingActionButton appListFabNew = ((FloatingActionButton) getActivity().findViewById(R.id.fab_new)); watchfaceDesignerActivity = mCoordinator.getWatchfaceDesignerActivity(); View rootView = inflater.inflate(R.layout.activity_appmanager, container, false); RecyclerView appListView = (RecyclerView) (rootView.findViewById(R.id.appListView)); appListView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { appListFab.hide(); appListFabNew.hide(); } else if (dy < 0) { appListFab.show(); if (watchfaceDesignerActivity != null) { appListFabNew.show(); } } } }); appListView.setLayoutManager(new GridAutoFitLayoutManager(getActivity(), 300)); mGBDeviceAppAdapter = new GBDeviceAppAdapter( appList, R.layout.item_appmanager_watchapp, this, mCoordinator.supportsAppReordering() || isCacheManager() ); appListView.setAdapter(mGBDeviceAppAdapter); ItemTouchHelper.Callback appItemTouchHelperCallback = new AppItemTouchHelperCallback(mGBDeviceAppAdapter); appManagementTouchHelper = new ItemTouchHelper(appItemTouchHelperCallback); appManagementTouchHelper.attachToRecyclerView(appListView); if ((watchfaceDesignerActivity != null) && (appListFabNew != null)) { appListFabNew.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent startIntent = new Intent(getContext(), watchfaceDesignerActivity); startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); getContext().startActivity(startIntent); } }); appListFabNew.show(); } return rootView; } @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (requestCode == CHILD_ACTIVITY_WATCHFACE_EDITOR) { refreshList(); } } protected void sendOrderToDevice(String concatFilename) { ArrayList uuids = new ArrayList<>(); for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getAppList()) { uuids.add(gbDeviceApp.getUUID()); } if (concatFilename != null) { ArrayList concatUuids = AppManagerActivity.getUuidsFromFile(concatFilename); uuids.addAll(concatUuids); } GBApplication.deviceService(mGBDevice).onAppReorder(uuids.toArray(new UUID[uuids.size()])); } public void onItemClick(View view, GBDeviceApp deviceApp) { openPopupMenu(view, deviceApp); } public boolean openPopupMenu(View view, GBDeviceApp deviceApp) { PopupMenu popupMenu = new PopupMenu(getContext(), view); popupMenu.getMenuInflater().inflate(R.menu.appmanager_context, popupMenu.getMenu()); Menu menu = popupMenu.getMenu(); final GBDeviceApp selectedApp = deviceApp; if (!selectedApp.isOnDevice() || selectedApp.getType() != GBDeviceApp.Type.WATCHFACE) { menu.removeItem(R.id.appmanager_watchface_activate); } if (!selectedApp.isOnDevice() || selectedApp.getType() != GBDeviceApp.Type.APP_GENERIC) { menu.removeItem(R.id.appmanager_app_start); } if (!selectedApp.isInCache()) { menu.removeItem(R.id.appmanager_app_edit); menu.removeItem(R.id.appmanager_app_reinstall); menu.removeItem(R.id.appmanager_app_share); menu.removeItem(R.id.appmanager_app_delete_cache); } if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) { menu.removeItem(R.id.appmanager_health_activate); menu.removeItem(R.id.appmanager_health_deactivate); } if (!PebbleProtocol.UUID_WORKOUT.equals(selectedApp.getUUID())) { menu.removeItem(R.id.appmanager_hrm_activate); menu.removeItem(R.id.appmanager_hrm_deactivate); } if (!PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) { menu.removeItem(R.id.appmanager_weather_activate); menu.removeItem(R.id.appmanager_weather_deactivate); menu.removeItem(R.id.appmanager_weather_install_provider); } if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM || selectedApp.getType() == GBDeviceApp.Type.WATCHFACE_SYSTEM) { menu.removeItem(R.id.appmanager_app_delete); } if (!selectedApp.isConfigurable()) { menu.removeItem(R.id.appmanager_app_configure); } if (PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) { PackageManager pm = getActivity().getPackageManager(); try { pm.getPackageInfo("ru.gelin.android.weather.notification", PackageManager.GET_ACTIVITIES); menu.removeItem(R.id.appmanager_weather_install_provider); } catch (PackageManager.NameNotFoundException e) { //menu.removeItem(R.id.appmanager_weather_activate); //menu.removeItem(R.id.appmanager_weather_deactivate); } } if ((mGBDevice.getType() != DeviceType.FOSSILQHYBRID) || (selectedApp.getType() != GBDeviceApp.Type.WATCHFACE)) { menu.removeItem(R.id.appmanager_app_edit); } if ((mGBDevice.getType() != DeviceType.FOSSILQHYBRID) || (!selectedApp.isOnDevice()) || ((selectedApp.getType() != GBDeviceApp.Type.WATCHFACE) && (selectedApp.getType() != GBDeviceApp.Type.APP_GENERIC))) { menu.removeItem(R.id.appmanager_app_download); } if (mGBDevice.getType() == DeviceType.FOSSILQHYBRID && selectedApp.getName().equals("workoutApp")) { menu.removeItem(R.id.appmanager_app_delete); } if (mGBDevice.getType() == DeviceType.PEBBLE) { switch (selectedApp.getType()) { case WATCHFACE: case APP_GENERIC: case APP_ACTIVITYTRACKER: break; default: menu.removeItem(R.id.appmanager_app_openinstore); } } else { menu.removeItem(R.id.appmanager_app_openinstore); } //menu.setHeaderTitle(selectedApp.getName()); popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { return onContextItemSelected(item, selectedApp); } } ); popupMenu.show(); return true; } private boolean onContextItemSelected(MenuItem item, GBDeviceApp selectedApp) { File appCacheDir; try { appCacheDir = mCoordinator.getAppCacheDir(); } catch (IOException e) { LOG.warn("could not get external dir while trying to access app cache."); return true; } switch (item.getItemId()) { case R.id.appmanager_app_delete_cache: deleteAppConfirm(selectedApp, true); return true; case R.id.appmanager_app_delete: deleteAppConfirm(selectedApp, false); return true; case R.id.appmanager_app_start: case R.id.appmanager_watchface_activate: GBApplication.deviceService(mGBDevice).onAppStart(selectedApp.getUUID(), true); return true; case R.id.appmanager_app_download: GBApplication.deviceService(mGBDevice).onAppDownload(selectedApp.getUUID()); GB.toast(getContext().getString(R.string.appmanager_download_started), Toast.LENGTH_LONG, GB.INFO); return true; case R.id.appmanager_app_reinstall: File cachePath = new File(appCacheDir, selectedApp.getUUID() + mCoordinator.getAppFileExtension()); GBApplication.deviceService(mGBDevice).onInstallApp(Uri.fromFile(cachePath)); return true; case R.id.appmanager_app_share: File origFilePath = new File(appCacheDir, selectedApp.getUUID() + mCoordinator.getAppFileExtension()); File appTempDir = new File(appCacheDir, "temp_sharing"); File sharedAppFile = new File(appTempDir, selectedApp.getName() + mCoordinator.getAppFileExtension()); try { appTempDir.mkdirs(); FileUtils.copyFile(origFilePath, sharedAppFile); } catch (IOException e) { return true; } Uri contentUri = FileProvider.getUriForFile(getContext(),getContext().getApplicationContext().getPackageName() + ".screenshot_provider", sharedAppFile); Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri); shareIntent.setType("*/*"); try { startActivity(Intent.createChooser(shareIntent, null)); } catch (ActivityNotFoundException e) { LOG.warn("Sharing watchface failed", e); } return true; case R.id.appmanager_health_activate: GBApplication.deviceService(mGBDevice).onInstallApp(Uri.parse("fake://health")); return true; case R.id.appmanager_hrm_activate: GBApplication.deviceService(mGBDevice).onInstallApp(Uri.parse("fake://hrm")); return true; case R.id.appmanager_weather_activate: GBApplication.deviceService(mGBDevice).onInstallApp(Uri.parse("fake://weather")); return true; case R.id.appmanager_health_deactivate: case R.id.appmanager_hrm_deactivate: case R.id.appmanager_weather_deactivate: GBApplication.deviceService(mGBDevice).onAppDelete(selectedApp.getUUID()); return true; case R.id.appmanager_weather_install_provider: startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/app/ru.gelin.android.weather.notification"))); return true; case R.id.appmanager_app_configure: GBApplication.deviceService(mGBDevice).onAppStart(selectedApp.getUUID(), true); Intent startIntent = new Intent(getContext().getApplicationContext(), ExternalPebbleJSActivity.class); startIntent.putExtra(DeviceService.EXTRA_APP_UUID, selectedApp.getUUID()); startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); startIntent.putExtra(ExternalPebbleJSActivity.SHOW_CONFIG, true); startActivity(startIntent); return true; case R.id.appmanager_app_openinstore: String url = "https://apps.rebble.io/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1/?native=true&?query=" + Uri.encode(selectedApp.getName()); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); startActivity(intent); return true; case R.id.appmanager_app_edit: Intent editWatchfaceIntent = new Intent(getContext(), watchfaceDesignerActivity); editWatchfaceIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); editWatchfaceIntent.putExtra(GBDevice.EXTRA_UUID, selectedApp.getUUID().toString()); startActivityForResult(editWatchfaceIntent, CHILD_ACTIVITY_WATCHFACE_EDITOR); return true; default: return super.onContextItemSelected(item); } } private void deleteAppConfirm(final GBDeviceApp selectedApp, final boolean deleteFromCache) { new MaterialAlertDialogBuilder(getContext()) .setTitle(R.string.Delete) .setMessage(requireContext().getString(R.string.contact_delete_confirm_description, selectedApp.getName())) .setIcon(R.drawable.ic_warning) .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { if (deleteFromCache) { deleteAppFromCache(selectedApp); } deleteAppFromDevice(selectedApp); }) .setNegativeButton(android.R.string.no, null) .show(); } private void deleteAppFromCache(final GBDeviceApp selectedApp) { final File appCacheDir; try { appCacheDir = mCoordinator.getAppCacheDir(); } catch (final IOException e) { LOG.warn("Could not get external dir while trying to access app cache", e); return; } String baseName = selectedApp.getUUID().toString(); String[] suffixToDelete = new String[]{mCoordinator.getAppFileExtension(), ".json", "_config.js", "_preset.json", ".png", "_preview.png", "_bg.png"}; for (String suffix : suffixToDelete) { File fileToDelete = new File(appCacheDir, baseName + suffix); if (!fileToDelete.delete()) { LOG.warn("Could not delete file from app cache: {}", fileToDelete); } else { LOG.debug("Deleted from app cache: {}", fileToDelete); } } AppManagerActivity.deleteFromAppOrderFile(getSortFilename(), selectedApp.getUUID()); // FIXME: only if successful Intent refreshIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST); LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(refreshIntent); } private void deleteAppFromDevice(final GBDeviceApp selectedApp) { if (mCoordinator.supportsAppReordering()) { AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchapps", selectedApp.getUUID()); // FIXME: only if successful AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchfaces", selectedApp.getUUID()); // FIXME: only if successful Intent refreshIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST); LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(refreshIntent); } GBApplication.deviceService(mGBDevice).onAppDelete(selectedApp.getUUID()); } /** * Sort an app list by the UUIDs in the sort filename. * * @param appList the app list to sort in-place */ protected void sortAppList(final List appList) { final ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename()); final Map uuidPosMap = new HashMap<>(); for (int i = 0; i < uuids.size(); i++) { uuidPosMap.put(uuids.get(i), i); } Collections.sort(appList, (a1, a2) -> { final Integer pos1 = uuidPosMap.get(a1.getUUID()); final Integer pos2 = uuidPosMap.get(a2.getUUID()); if (pos1 != null && pos2 != null) return Integer.compare(pos1, pos2); if (pos1 == null && pos2 == null) return a1.getName().compareToIgnoreCase(a2.getName()); if (pos1 != null) return -1; return 1; }); } @Override public void onDestroy() { LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver); super.onDestroy(); } public class AppItemTouchHelperCallback extends ItemTouchHelper.Callback { private final GBDeviceAppAdapter gbDeviceAppAdapter; public AppItemTouchHelperCallback(GBDeviceAppAdapter gbDeviceAppAdapter) { this.gbDeviceAppAdapter = gbDeviceAppAdapter; } @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { if (!mCoordinator.supportsAppReordering() && !isCacheManager()) { return 0; } //we only support up and down movement and only for moving, not for swiping apps away return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0); } @Override public boolean isLongPressDragEnabled() { return false; } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { gbDeviceAppAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition()); return true; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { //nothing to do } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); onChangedAppOrder(); } } }