From bae5bdb1f4df561a24dd5bb07df441c5af17e722 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 25 Apr 2017 11:08:13 +1000 Subject: [PATCH 01/10] Force apps in "Installed" list to sort by name. Previously they were left to be sorted however SQLite parsed the query. This turned out to result in them beign sorted by repos first, then names. For example, all of the GP apps would be at the bottom of the list. Fixes #965. --- app/src/main/java/org/fdroid/fdroid/data/AppProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java index 390b23a54..60056b62f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -696,6 +696,7 @@ public class AppProvider extends FDroidProvider { case INSTALLED: selection = selection.add(queryInstalled()); + sortOrder = Cols.NAME; includeSwap = false; break; From e2c82d2943e850fd1fa4cf323e805a83e3ba1c2a Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 25 Apr 2017 12:06:45 +1000 Subject: [PATCH 02/10] Accessibility tweaks in app details. * Correctly read out "Cancel download" * Allow users to hear download progress correctly. --- .../fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java | 4 ++++ app/src/main/res/layout/app_details2_header.xml | 3 +++ app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 9 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java index 7cc73539a..05c4f5624 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java @@ -372,17 +372,21 @@ public class AppDetailsRecyclerViewAdapter progressBar.setMax(totalBytes); progressBar.setProgress(bytesDownloaded); progressBar.setIndeterminate(totalBytes == -1); + progressLabel.setContentDescription(""); if (resIdString != 0) { progressLabel.setText(resIdString); + progressLabel.setContentDescription(context.getString(R.string.downloading)); progressPercent.setText(""); } else if (totalBytes > 0 && bytesDownloaded >= 0) { float percent = bytesDownloaded * 100 / totalBytes; progressLabel.setText(Utils.getFriendlySize(bytesDownloaded) + " / " + Utils.getFriendlySize(totalBytes)); + progressLabel.setContentDescription(context.getString(R.string.app__tts__downloading_progress, (int) percent)); NumberFormat format = NumberFormat.getPercentInstance(); format.setMaximumFractionDigits(0); progressPercent.setText(format.format(percent / 100)); } else if (bytesDownloaded >= 0) { progressLabel.setText(Utils.getFriendlySize(bytesDownloaded)); + progressLabel.setContentDescription(context.getString(R.string.downloading)); progressPercent.setText(""); } diff --git a/app/src/main/res/layout/app_details2_header.xml b/app/src/main/res/layout/app_details2_header.xml index f45bd2df8..d0e9cf401 100755 --- a/app/src/main/res/layout/app_details2_header.xml +++ b/app/src/main/res/layout/app_details2_header.xml @@ -90,6 +90,7 @@ android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_centerVertical="true" + android:contentDescription="@string/app__tts__cancel_download" android:src="@android:drawable/ic_menu_close_clear_cancel" /> Version %1$s (Recommended) New Added on %s + Cancel download Update Update %1$s @@ -475,6 +476,7 @@ New: Provided by %1$s. Downloading… + Downloading, %1$d%% complete Downloading %1$s Installing… Uninstalling… From 4cc6e5bc8966770a7e9c31b7e8aa2a7a1b006350 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 25 Apr 2017 12:26:45 +1000 Subject: [PATCH 03/10] Sort categories alphabetically by name. This pulls all the categories out of the database at once for sorting, rather than sorting in SQLite. This is to prevent having to store the localized category names in the database (and hence having to update them when the locale is changed). Fixes #967. --- .../views/categories/CategoryAdapter.java | 19 +++++---- .../views/categories/CategoryController.java | 8 +++- .../views/main/CategoriesViewBinder.java | 39 +++++++++++++++++-- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java index cf7f4b017..deb484acd 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java @@ -1,17 +1,21 @@ package org.fdroid.fdroid.views.categories; import android.app.Activity; -import android.database.Cursor; +import android.support.annotation.NonNull; import android.support.v4.app.LoaderManager; import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.data.Schema; + +import java.util.Collections; +import java.util.List; public class CategoryAdapter extends RecyclerView.Adapter { - private Cursor cursor; + @NonNull + private List unlocalizedCategoryNames = Collections.emptyList(); + private final Activity activity; private final LoaderManager loaderManager; @@ -27,17 +31,16 @@ public class CategoryAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(CategoryController holder, int position) { - cursor.moveToPosition(position); - holder.bindModel(cursor.getString(cursor.getColumnIndex(Schema.CategoryTable.Cols.NAME))); + holder.bindModel(unlocalizedCategoryNames.get(position)); } @Override public int getItemCount() { - return cursor == null ? 0 : cursor.getCount(); + return unlocalizedCategoryNames.size(); } - public void setCategoriesCursor(Cursor cursor) { - this.cursor = cursor; + public void setCategories(@NonNull List unlocalizedCategoryNames) { + this.unlocalizedCategoryNames = unlocalizedCategoryNames; notifyDataSetChanged(); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java index dc83000e4..7184066dd 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java @@ -75,11 +75,15 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade .build(); } + public static String translateCategory(Context context, String categoryName) { + int categoryNameId = getCategoryResource(context, categoryName, "string", false); + return categoryNameId == 0 ? categoryName : context.getString(categoryNameId); + } + void bindModel(@NonNull String categoryName) { currentCategory = categoryName; - int categoryNameId = getCategoryResource(activity, categoryName, "string", false); - String translatedName = categoryNameId == 0 ? categoryName : activity.getString(categoryNameId); + String translatedName = translateCategory(activity, categoryName); heading.setText(translatedName); heading.setContentDescription(activity.getString(R.string.tts_category_name, translatedName)); diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java index 462cf096b..4c7402ea7 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java @@ -19,6 +19,12 @@ import org.fdroid.fdroid.data.CategoryProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.categories.CategoryAdapter; +import org.fdroid.fdroid.views.categories.CategoryController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; /** * Responsible for ensuring that the categories view is inflated and then populated correctly. @@ -75,13 +81,38 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { ); } + /** + * Reads all categories from the cursor and stores them in memory to provide to the {@link CategoryAdapter}. + * + * It does this so it is easier to deal with localized/unlocalized categories without having + * to store the localized version in the database. It is not expected that the list of categories + * will grow so large as to make this a performance concern. If it does in the future, the + * {@link CategoryAdapter} can be reverted to wrap the cursor again, and localized category + * names can be stored in the database (allowing sorting in their localized form). + */ @Override public void onLoadFinished(Loader loader, Cursor cursor) { - if (loader.getId() != LOADER_ID) { + if (loader.getId() != LOADER_ID || cursor == null) { return; } - categoryAdapter.setCategoriesCursor(cursor); + List categoryNames = new ArrayList<>(cursor.getCount()); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + categoryNames.add(cursor.getString(cursor.getColumnIndex(Schema.CategoryTable.Cols.NAME))); + cursor.moveToNext(); + } + + Collections.sort(categoryNames, new Comparator() { + @Override + public int compare(String categoryOne, String categoryTwo) { + String localizedCategoryOne = CategoryController.translateCategory(activity, categoryOne); + String localizedCategoryTwo = CategoryController.translateCategory(activity, categoryTwo); + return localizedCategoryOne.compareTo(localizedCategoryTwo); + } + }); + + categoryAdapter.setCategories(categoryNames); if (categoryAdapter.getItemCount() == 0) { emptyState.setVisibility(View.VISIBLE); @@ -90,6 +121,8 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { emptyState.setVisibility(View.GONE); categoriesList.setVisibility(View.VISIBLE); } + + cursor.close(); } @Override @@ -98,7 +131,7 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { return; } - categoryAdapter.setCategoriesCursor(null); + categoryAdapter.setCategories(Collections.emptyList()); } } From 1eb224fc5e2ec6cf659fb74124d7820287561fc0 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 18 Apr 2017 18:24:07 +1000 Subject: [PATCH 04/10] Don't show run button for apps which cannot be launched. The feedback from ACRA was: > tried to run an app that doesn't have a launcher (termux api) --- .../views/apps/AppListItemController.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java index ae20dffa3..c9e387e23 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java @@ -322,7 +322,11 @@ public class AppListItemController extends RecyclerView.ViewHolder { actionButton.setVisibility(View.VISIBLE); if (wasSuccessfullyInstalled(app) != null) { - actionButton.setText(R.string.menu_launch); + if (activity.getPackageManager().getLaunchIntentForPackage(currentApp.packageName) != null) { + actionButton.setText(R.string.menu_launch); + } else { + actionButton.setVisibility(View.GONE); + } } else if (isReadyToInstall(app)) { if (app.isInstalled()) { actionButton.setText(R.string.app__install_downloaded_update); @@ -520,12 +524,14 @@ public class AppListItemController extends RecyclerView.ViewHolder { AppUpdateStatusManager.AppUpdateStatus successfullyInstalledStatus = wasSuccessfullyInstalled(currentApp); if (successfullyInstalledStatus != null) { Intent intent = activity.getPackageManager().getLaunchIntentForPackage(currentApp.packageName); - activity.startActivity(intent); + if (intent != null) { + activity.startActivity(intent); - // Once it is explicitly launched by the user, then we can pretty much forget about - // any sort of notification that the app was successfully installed. It should be - // apparent to the user because they just launched it. - AppUpdateStatusManager.getInstance(activity).removeApk(successfullyInstalledStatus.getUniqueKey()); + // Once it is explicitly launched by the user, then we can pretty much forget about + // any sort of notification that the app was successfully installed. It should be + // apparent to the user because they just launched it. + AppUpdateStatusManager.getInstance(activity).removeApk(successfullyInstalledStatus.getUniqueKey()); + } return; } From 6a0b16fc7d499060f79465b70a20b43a2e86b979 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 18 Apr 2017 18:31:30 +1000 Subject: [PATCH 05/10] Increase verbosity around crash site for better ACRA reports. Received the following crash report, where the user said it crashed while trying to install the priviledged extension: ``` java.lang.NullPointerException: Attempt to read from field 'android.content.pm.Signature[] android.content.pm.PackageInfo.signatures' on a null object reference at org.fdroid.fdroid.installer.ApkSignatureVerifier.getApkSignature(ApkSignatureVerifier.java:70) at org.fdroid.fdroid.installer.ApkSignatureVerifier.hasFDroidSignature(ApkSignatureVerifier.java:54) at org.fdroid.fdroid.installer.ExtensionInstaller.installPackageInternal(ExtensionInstaller.java:53) at org.fdroid.fdroid.installer.Installer.installPackage(Installer.java:265) at org.fdroid.fdroid.installer.InstallerService.onHandleIntent(InstallerService.java:77) at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65) at android.os.Handler.dispatchMessage(Handler.java:111) at android.os.Looper.loop(Looper.java:194) at android.os.HandlerThread.run(HandlerThread.java:61) ``` Not sure how to address it yet, so adding more specific excetpion for if it happens in the future. --- .../org/fdroid/fdroid/installer/ApkSignatureVerifier.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java index ee01a0543..e8479c22f 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java @@ -66,7 +66,15 @@ class ApkSignatureVerifier { private byte[] getApkSignature(File apkFile) { final String pkgPath = apkFile.getAbsolutePath(); + if (!apkFile.exists()) { + throw new IllegalArgumentException("Could not find APK at \"" + pkgPath + "\" when checking for signature."); + } + PackageInfo pkgInfo = pm.getPackageArchiveInfo(pkgPath, PackageManager.GET_SIGNATURES); + if (pkgInfo == null) { + throw new NullPointerException("Could not find PackageInfo for package at \"" + pkgPath + "\"."); + } + return signatureToBytes(pkgInfo.signatures); } From 2a6dcb63bbf678a972e66fefb0e0188926d2942f Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 25 Apr 2017 14:27:17 +1000 Subject: [PATCH 06/10] Check for `null` in `App#iconUrl`. Although I'm unsure of exactly why this is `null`, it seems sensible that there is a possibility of null icons (e.g. for .zip files or other media). As such, this just adds a guard condition to ensure that the `iconUrl` is not null. Fixes #981. --- app/src/main/java/org/fdroid/fdroid/FDroidApp.java | 3 +-- app/src/main/java/org/fdroid/fdroid/NotificationHelper.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 826d09122..078bd7ce8 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -279,8 +279,7 @@ public class FDroidApp extends Application { new FileNameGenerator() { @Override public String generate(String imageUri) { - return imageUri.substring( - imageUri.lastIndexOf('/') + 1); + return imageUri.substring(imageUri.lastIndexOf('/') + 1); } }, // 30 days in secs: 30*24*60*60 = 2592000 diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 6737c49cc..16c869b1e 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -531,7 +531,7 @@ class NotificationHelper { // Need to check that the notification is still valid, and also that the image // is indeed cached now, so we won't get stuck in an endless loop. AppUpdateStatusManager.AppUpdateStatus oldEntry = appUpdateStatusManager.get(entry.getUniqueKey()); - if (oldEntry != null && DiskCacheUtils.findInCache(oldEntry.app.iconUrl, ImageLoader.getInstance().getDiskCache()) != null) { + if (oldEntry != null && oldEntry.app != null && oldEntry.app.iconUrl != null && DiskCacheUtils.findInCache(oldEntry.app.iconUrl, ImageLoader.getInstance().getDiskCache()) != null) { createNotification(oldEntry); // Update with new image! } } From 4b70d81e5c2f39c0a733dd54f4847dd5311dba11 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 25 Apr 2017 14:48:59 +1000 Subject: [PATCH 07/10] Make install/uninstall/run/upgrade buttons not overlap last updated text Instead of showing them below the icon, it now puts the icon + name + author + last updated into a single layout which can grow if the app name or author wraps to a second line. The buttons are now below this additional layout. --- .../main/res/layout/app_details2_header.xml | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/app/src/main/res/layout/app_details2_header.xml b/app/src/main/res/layout/app_details2_header.xml index d0e9cf401..a6205c616 100755 --- a/app/src/main/res/layout/app_details2_header.xml +++ b/app/src/main/res/layout/app_details2_header.xml @@ -26,53 +26,63 @@ android:layout_marginRight="8dp" > - - - + + android:paddingBottom="8dp"> - + + + + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:layout_alignParentTop="true" + android:layout_marginTop="8dp" + android:layout_toEndOf="@id/icon" + android:layout_toRightOf="@id/icon" + android:orientation="vertical"> - + - + - + + + + + + + android:layout_below="@id/icon_and_name" + tools:visibility="gone"> +