Merge branch '0.103-alpha3--ui-fixes' into 'master'

0.103-alpha3  UI fixes

Closes #981, #967, and #965

See merge request !490
This commit is contained in:
Hans-Christoph Steiner 2017-04-26 10:34:02 +00:00
commit f8c9d7ec8e
11 changed files with 153 additions and 65 deletions

View File

@ -279,8 +279,7 @@ public class FDroidApp extends Application {
new FileNameGenerator() { new FileNameGenerator() {
@Override @Override
public String generate(String imageUri) { public String generate(String imageUri) {
return imageUri.substring( return imageUri.substring(imageUri.lastIndexOf('/') + 1);
imageUri.lastIndexOf('/') + 1);
} }
}, },
// 30 days in secs: 30*24*60*60 = 2592000 // 30 days in secs: 30*24*60*60 = 2592000

View File

@ -531,7 +531,10 @@ class NotificationHelper {
// Need to check that the notification is still valid, and also that the image // 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. // is indeed cached now, so we won't get stuck in an endless loop.
AppUpdateStatusManager.AppUpdateStatus oldEntry = appUpdateStatusManager.get(entry.getUniqueKey()); 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! createNotification(oldEntry); // Update with new image!
} }
} }

View File

@ -696,6 +696,7 @@ public class AppProvider extends FDroidProvider {
case INSTALLED: case INSTALLED:
selection = selection.add(queryInstalled()); selection = selection.add(queryInstalled());
sortOrder = Cols.NAME;
includeSwap = false; includeSwap = false;
break; break;

View File

@ -25,6 +25,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.Signature; import android.content.pm.Signature;
import org.acra.ACRA;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.spongycastle.util.encoders.Hex; import org.spongycastle.util.encoders.Hex;
@ -51,6 +52,15 @@ class ApkSignatureVerifier {
} }
public boolean hasFDroidSignature(File apkFile) { 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[] apkSig = getApkSignature(apkFile);
byte[] fdroidSig = getFDroidSignature(); byte[] fdroidSig = getFDroidSignature();
@ -66,7 +76,15 @@ class ApkSignatureVerifier {
private byte[] getApkSignature(File apkFile) { private byte[] getApkSignature(File apkFile) {
final String pkgPath = apkFile.getAbsolutePath(); 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); 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); return signatureToBytes(pkgInfo.signatures);
} }

View File

@ -372,17 +372,21 @@ public class AppDetailsRecyclerViewAdapter
progressBar.setMax(totalBytes); progressBar.setMax(totalBytes);
progressBar.setProgress(bytesDownloaded); progressBar.setProgress(bytesDownloaded);
progressBar.setIndeterminate(totalBytes == -1); progressBar.setIndeterminate(totalBytes == -1);
progressLabel.setContentDescription("");
if (resIdString != 0) { if (resIdString != 0) {
progressLabel.setText(resIdString); progressLabel.setText(resIdString);
progressLabel.setContentDescription(context.getString(R.string.downloading));
progressPercent.setText(""); progressPercent.setText("");
} else if (totalBytes > 0 && bytesDownloaded >= 0) { } else if (totalBytes > 0 && bytesDownloaded >= 0) {
float percent = bytesDownloaded * 100 / totalBytes; float percent = bytesDownloaded * 100 / totalBytes;
progressLabel.setText(Utils.getFriendlySize(bytesDownloaded) + " / " + Utils.getFriendlySize(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(); NumberFormat format = NumberFormat.getPercentInstance();
format.setMaximumFractionDigits(0); format.setMaximumFractionDigits(0);
progressPercent.setText(format.format(percent / 100)); progressPercent.setText(format.format(percent / 100));
} else if (bytesDownloaded >= 0) { } else if (bytesDownloaded >= 0) {
progressLabel.setText(Utils.getFriendlySize(bytesDownloaded)); progressLabel.setText(Utils.getFriendlySize(bytesDownloaded));
progressLabel.setContentDescription(context.getString(R.string.downloading));
progressPercent.setText(""); progressPercent.setText("");
} }

View File

@ -322,7 +322,11 @@ public class AppListItemController extends RecyclerView.ViewHolder {
actionButton.setVisibility(View.VISIBLE); actionButton.setVisibility(View.VISIBLE);
if (wasSuccessfullyInstalled(app) != null) { 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)) { } else if (isReadyToInstall(app)) {
if (app.isInstalled()) { if (app.isInstalled()) {
actionButton.setText(R.string.app__install_downloaded_update); actionButton.setText(R.string.app__install_downloaded_update);
@ -520,12 +524,14 @@ public class AppListItemController extends RecyclerView.ViewHolder {
AppUpdateStatusManager.AppUpdateStatus successfullyInstalledStatus = wasSuccessfullyInstalled(currentApp); AppUpdateStatusManager.AppUpdateStatus successfullyInstalledStatus = wasSuccessfullyInstalled(currentApp);
if (successfullyInstalledStatus != null) { if (successfullyInstalledStatus != null) {
Intent intent = activity.getPackageManager().getLaunchIntentForPackage(currentApp.packageName); 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 // 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 // any sort of notification that the app was successfully installed. It should be
// apparent to the user because they just launched it. // apparent to the user because they just launched it.
AppUpdateStatusManager.getInstance(activity).removeApk(successfullyInstalledStatus.getUniqueKey()); AppUpdateStatusManager.getInstance(activity).removeApk(successfullyInstalledStatus.getUniqueKey());
}
return; return;
} }

View File

@ -1,17 +1,21 @@
package org.fdroid.fdroid.views.categories; package org.fdroid.fdroid.views.categories;
import android.app.Activity; import android.app.Activity;
import android.database.Cursor; import android.support.annotation.NonNull;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.fdroid.fdroid.R; 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<CategoryController> { public class CategoryAdapter extends RecyclerView.Adapter<CategoryController> {
private Cursor cursor; @NonNull
private List<String> unlocalizedCategoryNames = Collections.<String>emptyList();
private final Activity activity; private final Activity activity;
private final LoaderManager loaderManager; private final LoaderManager loaderManager;
@ -27,17 +31,16 @@ public class CategoryAdapter extends RecyclerView.Adapter<CategoryController> {
@Override @Override
public void onBindViewHolder(CategoryController holder, int position) { public void onBindViewHolder(CategoryController holder, int position) {
cursor.moveToPosition(position); holder.bindModel(unlocalizedCategoryNames.get(position));
holder.bindModel(cursor.getString(cursor.getColumnIndex(Schema.CategoryTable.Cols.NAME)));
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return cursor == null ? 0 : cursor.getCount(); return unlocalizedCategoryNames.size();
} }
public void setCategoriesCursor(Cursor cursor) { public void setCategories(@NonNull List<String> unlocalizedCategoryNames) {
this.cursor = cursor; this.unlocalizedCategoryNames = unlocalizedCategoryNames;
notifyDataSetChanged(); notifyDataSetChanged();
} }

View File

@ -75,11 +75,15 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade
.build(); .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) { void bindModel(@NonNull String categoryName) {
currentCategory = categoryName; currentCategory = categoryName;
int categoryNameId = getCategoryResource(activity, categoryName, "string", false); String translatedName = translateCategory(activity, categoryName);
String translatedName = categoryNameId == 0 ? categoryName : activity.getString(categoryNameId);
heading.setText(translatedName); heading.setText(translatedName);
heading.setContentDescription(activity.getString(R.string.tts_category_name, translatedName)); heading.setContentDescription(activity.getString(R.string.tts_category_name, translatedName));

View File

@ -19,6 +19,12 @@ import org.fdroid.fdroid.data.CategoryProvider;
import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.apps.AppListActivity;
import org.fdroid.fdroid.views.categories.CategoryAdapter; 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. * Responsible for ensuring that the categories view is inflated and then populated correctly.
@ -75,13 +81,38 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
); );
} }
/**
* 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 @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (loader.getId() != LOADER_ID) { if (loader.getId() != LOADER_ID || cursor == null) {
return; return;
} }
categoryAdapter.setCategoriesCursor(cursor); List<String> 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<String>() {
@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) { if (categoryAdapter.getItemCount() == 0) {
emptyState.setVisibility(View.VISIBLE); emptyState.setVisibility(View.VISIBLE);
@ -90,6 +121,8 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
emptyState.setVisibility(View.GONE); emptyState.setVisibility(View.GONE);
categoriesList.setVisibility(View.VISIBLE); categoriesList.setVisibility(View.VISIBLE);
} }
cursor.close();
} }
@Override @Override
@ -98,7 +131,7 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
return; return;
} }
categoryAdapter.setCategoriesCursor(null); categoryAdapter.setCategories(Collections.<String>emptyList());
} }
} }

View File

@ -26,53 +26,63 @@
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
> >
<ImageView <!-- Icon, Name, Author (optional), Updated date -->
android:id="@+id/icon" <RelativeLayout
android:layout_width="72dp" android:id="@+id/icon_and_name"
android:layout_height="72dp" android:layout_width="match_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:paddingBottom="8dp"
android:paddingRight="8dp"
android:paddingEnd="8dp"
android:src="@drawable/ic_repo_app_default" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:paddingBottom="8dp">
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">
<TextView <ImageView
android:id="@+id/title" android:id="@+id/icon"
android:layout_width="match_parent" android:layout_width="72dp"
android:layout_height="72dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:paddingRight="8dp"
android:paddingEnd="8dp"
android:src="@drawable/ic_repo_app_default" />
<!-- Name, Author (optional), Updated date -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/DetailsAppTitleStyle" android:layout_alignParentEnd="true"
tools:text="App Title" /> 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">
<TextView <TextView
android:id="@+id/author" android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/DetailsAppTitleStyle"
tools:text="Author" /> tools:text="Really Long App Title Which Wraps Around Onto the Second Line" />
<TextView <TextView
android:id="@+id/text_last_update" android:id="@+id/author"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/DetailsLastUpdatedStyle" android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/darker_gray" tools:text="Author" />
tools:text="Update released 12 days ago" />
</LinearLayout> <TextView
android:id="@+id/text_last_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/DetailsLastUpdatedStyle"
android:textColor="@android:color/darker_gray"
tools:text="Update released 12 days ago" />
</LinearLayout>
</RelativeLayout>
<!-- Progress info -->
<RelativeLayout <RelativeLayout
android:id="@+id/progress_layout" android:id="@+id/progress_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -81,7 +91,8 @@
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@id/icon"> android:layout_below="@id/icon_and_name"
tools:visibility="gone">
<ImageView <ImageView
android:id="@+id/progress_cancel" android:id="@+id/progress_cancel"
@ -90,6 +101,7 @@
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:contentDescription="@string/app__tts__cancel_download"
android:src="@android:drawable/ic_menu_close_clear_cancel" /> android:src="@android:drawable/ic_menu_close_clear_cancel" />
<TextView <TextView
@ -99,6 +111,8 @@
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:text="@string/downloading" android:text="@string/downloading"
android:contentDescription="@string/downloading"
android:focusable="true"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView <TextView
@ -122,18 +136,19 @@
android:layout_toStartOf="@id/progress_cancel" /> android:layout_toStartOf="@id/progress_cancel" />
</RelativeLayout> </RelativeLayout>
<!-- Install, Uninstall, Upgrade, Run buttons -->
<LinearLayout <LinearLayout
android:id="@+id/button_layout" android:id="@+id/button_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/icon" android:layout_below="@id/icon_and_name"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:clipToPadding="false" android:clipToPadding="false"
android:visibility="gone" android:visibility="visible"
> >
<Button <Button

View File

@ -75,6 +75,7 @@
<string name="app_recommended_version_installed">Version %1$s (Recommended)</string> <string name="app_recommended_version_installed">Version %1$s (Recommended)</string>
<string name="app__newly_added">New</string> <string name="app__newly_added">New</string>
<string name="added_on">Added on %s</string> <string name="added_on">Added on %s</string>
<string name="app__tts__cancel_download">Cancel download</string>
<string name="app__install_downloaded_update">Update</string> <string name="app__install_downloaded_update">Update</string>
<string name="app_list__name__downloaded_and_ready_to_update">Update %1$s</string> <string name="app_list__name__downloaded_and_ready_to_update">Update %1$s</string>
@ -475,6 +476,7 @@
<string name="perms_new_perm_prefix">New:</string> <string name="perms_new_perm_prefix">New:</string>
<string name="perms_description_app">Provided by %1$s.</string> <string name="perms_description_app">Provided by %1$s.</string>
<string name="downloading">Downloading…</string> <string name="downloading">Downloading…</string>
<string name="app__tts__downloading_progress">Downloading, %1$d%% complete</string>
<string name="downloading_apk">Downloading %1$s</string> <string name="downloading_apk">Downloading %1$s</string>
<string name="installing">Installing…</string> <string name="installing">Installing…</string>
<string name="uninstalling">Uninstalling…</string> <string name="uninstalling">Uninstalling…</string>