diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ee80bbfd..b7da61133 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -170,6 +170,10 @@ android:name=".activities.charts.ChartsPreferencesActivity" android:label="@string/activity_prefs_charts" android:parentActivityName=".activities.charts.ChartsPreferencesActivity" /> + directory_listing = new ArrayAdapter(DataManagementActivity.this, android.R.layout.simple_list_item_1, export_path.list()); - - builder.setSingleChoiceItems(directory_listing, 0, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }); - - AlertDialog dialog = builder.create(); - dialog.show(); - - } + showContentDataButton.setOnClickListener(v -> { + final Intent fileManagerIntent = new Intent(DataManagementActivity.this, FileManagerActivity.class); + startActivity(fileManagerIntent); }); int oldDBVisibility = hasOldActivityDatabase() ? View.VISIBLE : View.GONE; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerActivity.java new file mode 100644 index 000000000..6e5412d4f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerActivity.java @@ -0,0 +1,89 @@ +/* Copyright (C) 2024 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 . */ +package nodomain.freeyourgadget.gadgetbridge.activities.files; + +import android.os.Bundle; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class FileManagerActivity extends AbstractGBActivity { + private static final Logger LOG = LoggerFactory.getLogger(FileManagerActivity.class); + + public static final String EXTRA_PATH = "path"; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_file_manager); + + final RecyclerView fileListView = findViewById(R.id.fileListView); + fileListView.setLayoutManager(new LinearLayoutManager(this)); + + final File directory; + if (getIntent().hasExtra(EXTRA_PATH)) { + directory = new File(getIntent().getStringExtra(EXTRA_PATH)); + } else { + try { + directory = FileUtils.getExternalFilesDir(); + } catch (final IOException e) { + GB.toast("Failed to list external files dir", Toast.LENGTH_LONG, GB.ERROR); + LOG.error("Failed to list external files dir", e); + finish(); + return; + } + } + + if (!directory.isDirectory()) { + GB.toast("Not a directory", Toast.LENGTH_LONG, GB.ERROR); + finish(); + return; + } + + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(directory.getName()); + } + + final FileManagerAdapter appListAdapter = new FileManagerAdapter(this, directory); + + fileListView.setAdapter(appListAdapter); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerAdapter.java new file mode 100644 index 000000000..e64d0245b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerAdapter.java @@ -0,0 +1,152 @@ +/* Copyright (C) 2023-2024 akasaka / Genjitsu Labs + + 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.files; + +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.RecyclerView; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class FileManagerAdapter extends RecyclerView.Adapter { + protected static final Logger LOG = LoggerFactory.getLogger(FileManagerAdapter.class); + + private final List fileList; + private final Context mContext; + + public FileManagerAdapter(final Context context, final File directory) { + mContext = context; + + // FIXME: This can be slow, make it async + fileList = Arrays.asList(directory.listFiles()); + fileList.sort((f1, f2) -> { + if (f1.isDirectory() && f2.isFile()) + return -1; + if (f1.isFile() && f2.isDirectory()) + return 1; + + return String.CASE_INSENSITIVE_ORDER.compare(f1.getName(), f2.getName()); + }); + } + + @NonNull + @Override + public FileManagerViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final View view = LayoutInflater.from(mContext).inflate(R.layout.item_file_manager, parent, false); + return new FileManagerViewHolder(view); + } + + @Override + public void onBindViewHolder(final FileManagerViewHolder holder, int position) { + final File file = fileList.get(position); + + holder.name.setText(file.getName()); + if (file.isDirectory()) { + holder.icon.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_folder)); + holder.description.setVisibility(View.GONE); + holder.menu.setVisibility(View.GONE); + } else { + holder.icon.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_file_open)); + holder.description.setText(formatFileSize(file.length())); + holder.description.setVisibility(View.VISIBLE); + holder.menu.setVisibility(View.VISIBLE); + holder.menu.setOnClickListener(view -> { + final PopupMenu menu = new PopupMenu(mContext, holder.menu); + menu.inflate(R.menu.file_manager_file); + menu.setOnMenuItemClickListener(item -> { + final int itemId = item.getItemId(); + if (itemId == R.id.file_manager_file_menu_share) { + try { + AndroidUtils.shareFile(mContext, file, "*/*"); + } catch (final IOException e) { + GB.toast("Failed to share file", Toast.LENGTH_LONG, GB.ERROR, e); + } + return true; + } + + return false; + }); + menu.show(); + }); + } + + holder.itemView.setOnClickListener(v -> { + if (file.isDirectory()) { + final Intent fileManagerIntent = new Intent(mContext, FileManagerActivity.class); + fileManagerIntent.putExtra(FileManagerActivity.EXTRA_PATH, file.getPath()); + mContext.startActivity(fileManagerIntent); + } else { + try { + AndroidUtils.viewFile(file.getAbsolutePath(), "*/*", mContext); + } catch (final IOException e) { + GB.toast("Failed to open file", Toast.LENGTH_LONG, GB.ERROR, e); + } + } + }); + } + + @Override + public int getItemCount() { + return fileList.size(); + } + + public static class FileManagerViewHolder extends RecyclerView.ViewHolder { + final ImageView icon; + final TextView name; + final TextView description; + final ImageView menu; + + FileManagerViewHolder(View itemView) { + super(itemView); + + icon = itemView.findViewById(R.id.file_icon); + name = itemView.findViewById(R.id.file_name); + description = itemView.findViewById(R.id.file_description); + menu = itemView.findViewById(R.id.file_menu); + } + } + + private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("#,##0.#"); + + public static String formatFileSize(final long size) { + if (size <= 0) return "0"; + final String[] units = new String[]{"B", "kB", "MB", "GB", "TB", "PB", "EB"}; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return SIZE_FORMAT.format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java index 8d36a6369..4c81daf23 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java @@ -265,14 +265,14 @@ public class AndroidUtils { throw new IllegalArgumentException("Unable to decode the given uri to a file path: " + uri); } - public static void viewFile(String path, String action, Context context) throws IOException { - Intent intent = new Intent(action); + public static void viewFile(String path, String mimeType, Context context) throws IOException { + Intent intent = new Intent(Intent.ACTION_VIEW); File file = new File(path); Uri contentUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".screenshot_provider", file); intent.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setDataAndType(contentUri,"application/gpx+xml"); + intent.setDataAndType(contentUri, mimeType); try { context.startActivity(intent); } catch (ActivityNotFoundException e) { diff --git a/app/src/main/res/layout/activity_file_manager.xml b/app/src/main/res/layout/activity_file_manager.xml new file mode 100644 index 000000000..bdd6d30ec --- /dev/null +++ b/app/src/main/res/layout/activity_file_manager.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/layout/item_file_manager.xml b/app/src/main/res/layout/item_file_manager.xml new file mode 100644 index 000000000..36c576122 --- /dev/null +++ b/app/src/main/res/layout/item_file_manager.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/file_manager_file.xml b/app/src/main/res/menu/file_manager_file.xml new file mode 100644 index 000000000..ace364415 --- /dev/null +++ b/app/src/main/res/menu/file_manager_file.xml @@ -0,0 +1,6 @@ + + + +