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..40fba8548 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -531,7 +531,10 @@ 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! } } 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; 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..fd4a9d789 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java @@ -25,6 +25,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import org.acra.ACRA; import org.fdroid.fdroid.Utils; import org.spongycastle.util.encoders.Hex; @@ -51,6 +52,15 @@ class ApkSignatureVerifier { } public boolean hasFDroidSignature(File apkFile) { + if (!apkFile.exists()) { + ACRA.getErrorReporter().handleException( + new Exception("Failed to install Privileged Extension, because " + apkFile.getAbsolutePath() + " does not exist."), + false + ); + + return false; + } + byte[] apkSig = getApkSignature(apkFile); byte[] fdroidSig = getFDroidSignature(); @@ -66,7 +76,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 IllegalArgumentException("Could not find PackageInfo for package at \"" + pkgPath + "\"."); + } + return signatureToBytes(pkgInfo.signatures); } 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/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; } 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()); } } diff --git a/app/src/main/res/layout/app_details2_header.xml b/app/src/main/res/layout/app_details2_header.xml index f45bd2df8..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"> +