Introduce FileInstaller, a way to handle media files from F-Droid

* This installer is invoked when for non-apk/media files, and
  copies them to an appropriate folder on the sdcard.
* We also introduce a FileInstallerActivity to ask for storage
  permissions at runtime, as needed by Android 6.0 and above,
  and handle the install/uninstall process.
* A toast is shown with the install path after installation.

TODO:
* Manage Installed Apps screen doesn't show media files.
This commit is contained in:
Chirayu Desai 2017-06-20 16:01:56 +05:30
parent cbf3133e43
commit 0d8b0c7fd4
48 changed files with 346 additions and 71 deletions

View File

@ -508,6 +508,7 @@
</activity>
<activity android:name=".AboutActivity" android:theme="@style/Theme.AppCompat.Light.Dialog" />
<activity android:name=".installer.FileInstallerActivity" android:theme="@style/AppThemeTransparent" />
</application>

View File

@ -310,7 +310,7 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name);
shareIntent.putExtra(Intent.EXTRA_TEXT, app.name + " (" + app.summary + ") - https://f-droid.org/app/" + app.packageName);
boolean showNearbyItem = app.isInstalled() && fdroidApp.bluetoothAdapter != null;
boolean showNearbyItem = app.isInstalled(getApplicationContext()) && fdroidApp.bluetoothAdapter != null;
ShareChooserDialog.createChooser((CoordinatorLayout) findViewById(R.id.rootCoordinator), this, this, shareIntent, showNearbyItem);
return true;
} else if (item.getItemId() == R.id.action_ignore_all) {
@ -778,8 +778,12 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
Apk apk = app.installedApk;
if (apk == null) {
// TODO ideally, app would be refreshed immediately after install, then this
// workaround would be unnecessary
apk = getInstalledApk();
// workaround would be unnecessary - unless it is a media file
apk = app.getMediaApkifInstalled(getApplicationContext());
if (apk == null) {
// When the app isn't a media file - the above workaround refers to this.
apk = getInstalledApk();
}
app.installedApk = apk;
}
Installer installer = InstallerFactory.create(this, apk);

View File

@ -39,12 +39,12 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiskCache;
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import info.guardianproject.netcipher.NetCipher;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
@ -55,12 +55,11 @@ import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProviderService;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.installer.ApkFileProvider;
import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.installer.ApkFileProvider;
import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.net.ImageLoaderForUIL;
import org.fdroid.fdroid.net.WifiStateChangeService;
import sun.net.www.protocol.bluetooth.Handler;
import java.io.IOException;
import java.net.URL;
@ -69,6 +68,10 @@ import java.net.URLStreamHandlerFactory;
import java.security.Security;
import java.util.List;
import info.guardianproject.netcipher.NetCipher;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import sun.net.www.protocol.bluetooth.Handler;
@ReportsCrashes(mailTo = "reports@f-droid.org",
mode = ReportingInteractionMode.DIALOG,
reportDialogClass = org.fdroid.fdroid.acra.CrashReportActivity.class,
@ -80,6 +83,8 @@ public class FDroidApp extends Application {
public static final String SYSTEM_DIR_NAME = Environment.getRootDirectory().getAbsolutePath();
private static FDroidApp instance;
// for the local repo on this device, all static since there is only one
public static volatile int port;
public static volatile String ipAddressString;
@ -204,6 +209,7 @@ public class FDroidApp extends Application {
@Override
public void onCreate() {
super.onCreate();
instance = this;
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
@ -449,4 +455,8 @@ public class FDroidApp extends Application {
public static boolean isUsingTor() {
return useTor;
}
public static Context getInstance() {
return instance;
}
}

View File

@ -248,7 +248,7 @@ class NotificationHelper {
case Downloading:
return app.name;
case ReadyToInstall:
return context.getString(app.isInstalled() ? R.string.notification_title_single_ready_to_install_update : R.string.notification_title_single_ready_to_install);
return context.getString(app.isInstalled(context) ? R.string.notification_title_single_ready_to_install_update : R.string.notification_title_single_ready_to_install);
case Installing:
return app.name;
case Installed:
@ -264,7 +264,7 @@ class NotificationHelper {
case UpdateAvailable:
return app.name;
case Downloading:
return context.getString(app.isInstalled() ? R.string.notification_content_single_downloading_update : R.string.notification_content_single_downloading, app.name);
return context.getString(app.isInstalled(context) ? R.string.notification_content_single_downloading_update : R.string.notification_content_single_downloading, app.name);
case ReadyToInstall:
return app.name;
case Installing:
@ -282,9 +282,9 @@ class NotificationHelper {
case UpdateAvailable:
return context.getString(R.string.notification_title_summary_update_available);
case Downloading:
return context.getString(app.isInstalled() ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading);
return context.getString(app.isInstalled(context) ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading);
case ReadyToInstall:
return context.getString(app.isInstalled() ? R.string.notification_title_summary_ready_to_install_update : R.string.notification_title_summary_ready_to_install);
return context.getString(app.isInstalled(context) ? R.string.notification_title_summary_ready_to_install_update : R.string.notification_title_summary_ready_to_install);
case Installing:
return context.getString(R.string.notification_title_summary_installing);
case Installed:

View File

@ -2,15 +2,21 @@ package org.fdroid.fdroid.data;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.database.Cursor;
import android.os.Build;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
@ -470,4 +476,55 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
}
requestedPermissions = set.toArray(new String[set.size()]);
}
/**
* Get the install path for a "non-apk" media file
* Defaults to {@link android.os.Environment#DIRECTORY_DOWNLOADS}
*
* @return the install path for this {@link Apk}
*/
public File getMediaInstallPath(Context context) {
File path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS); // Default for all other non-apk/media files
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(this.getUrl());
if (TextUtils.isEmpty(fileExtension)) return path;
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
String[] mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension).split("/");
String topLevelType;
if (mimeType.length == 0) {
topLevelType = "";
} else {
topLevelType = mimeType[0];
}
if ("audio".equals(topLevelType)) {
path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MUSIC);
} else if ("image".equals(topLevelType)) {
path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
} else if ("video".equals(topLevelType)) {
path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES);
// TODO support OsmAnd map files, other map apps?
//} else if (mimeTypeMap.hasExtension("map")) { // OsmAnd map files
//} else if (this.apkName.matches(".*.ota_[0-9]*.zip")) { // Over-The-Air update ZIP files
} else if (this.apkName.endsWith(".zip")) { // Over-The-Air update ZIP files
path = new File(context.getApplicationInfo().dataDir + "/ota");
}
return path;
}
public boolean isMediaInstalled(Context context) {
return new File(this.getMediaInstallPath(context), this.apkName).isFile();
}
/**
* Default to assuming apk if apkName is null since that has always been
* what we had.
* @return true if this is an apk instead of a non-apk/media file
*/
public boolean isApk() {
return this.apkName == null || this.apkName.endsWith(".apk");
}
}

View File

@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.fdroid.fdroid.AppFilter;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
import org.xmlpull.v1.XmlPullParser;
@ -42,6 +43,7 @@ import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@ -853,8 +855,38 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return values;
}
public boolean isInstalled() {
return installedVersionCode > 0;
public boolean isInstalled(Context context) {
return isMediaInstalled(context) || installedVersionCode > 0;
}
public boolean isMediaInstalled(Context context) {
return getMediaApkifInstalled(context) != null;
}
/**
* Gets the installed media apk from all the apks of this {@link App}, if any.
*
* @return The installed media {@link Apk} if it exists, null otherwise.
*/
public Apk getMediaApkifInstalled(Context context) {
// This is always null for media files. We could skip the code below completely if it wasn't
if (this.installedApk != null && !this.installedApk.isApk() && this.installedApk.isMediaInstalled(context)) {
return this.installedApk;
}
// This code comes from AppDetailsRecyclerViewAdapter
final List<Apk> apks = ApkProvider.Helper.findByPackageName(context, this.packageName);
for (final Apk apk : apks) {
boolean allowByCompatability = apk.compatible || Preferences.get().showIncompatibleVersions();
boolean allowBySig = this.installedSig == null || TextUtils.equals(this.installedSig, apk.sig);
if (allowByCompatability && allowBySig) {
if (!apk.isApk()) {
if (apk.isMediaInstalled(context)) {
return apk;
}
}
}
}
return null;
}
/**
@ -966,11 +998,10 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return 0;
}
/**
* System apps aren't uninstallable, only their updates are.
*/
public boolean isUninstallable(Context context) {
if (this.isInstalled()) {
if (this.isMediaInstalled(context)) {
return true;
} else if (this.isInstalled(context)) {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo;
try {
@ -980,8 +1011,9 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return false;
}
// System apps aren't uninstallable.
final boolean isSystem = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
return !isSystem && this.isInstalled();
return !isSystem && this.isInstalled(context);
} else {
return false;
}

View File

@ -19,15 +19,16 @@
package org.fdroid.fdroid.installer;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import org.fdroid.fdroid.data.Apk;
public class DummyInstaller extends Installer {
public class FileInstaller extends Installer {
public DummyInstaller(Context context, Apk apk) {
public FileInstaller(Context context, Apk apk) {
super(context, apk);
}
@ -43,17 +44,41 @@ public class DummyInstaller extends Installer {
@Override
public void installPackage(Uri localApkUri, Uri downloadUri) {
// Do nothing
installPackageInternal(localApkUri, downloadUri);
}
@Override
protected void installPackageInternal(Uri localApkUri, Uri downloadUri) {
// Do nothing
Intent installIntent = new Intent(context, FileInstallerActivity.class);
installIntent.setAction(FileInstallerActivity.ACTION_INSTALL_FILE);
installIntent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri);
installIntent.putExtra(Installer.EXTRA_APK, apk);
installIntent.setData(localApkUri);
PendingIntent installPendingIntent = PendingIntent.getActivity(
context.getApplicationContext(),
localApkUri.hashCode(),
installIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_USER_INTERACTION,
installPendingIntent);
}
@Override
protected void uninstallPackage() {
// Do nothing
sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED);
Intent uninstallIntent = new Intent(context, FileInstallerActivity.class);
uninstallIntent.setAction(FileInstallerActivity.ACTION_UNINSTALL_FILE);
uninstallIntent.putExtra(Installer.EXTRA_APK, apk);
PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
context.getApplicationContext(),
apk.packageName.hashCode(),
uninstallIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastUninstall(Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
}
@Override

View File

@ -0,0 +1,179 @@
package org.fdroid.fdroid.installer;
import android.Manifest;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.view.ContextThemeWrapper;
import android.widget.Toast;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import java.io.File;
import java.io.IOException;
public class FileInstallerActivity extends FragmentActivity {
private static final String TAG = "FileInstallerActivity";
private static final int MY_PERMISSIONS_REQUEST_STORAGE = 1;
static final String ACTION_INSTALL_FILE
= "org.fdroid.fdroid.installer.FileInstaller.action.INSTALL_PACKAGE";
static final String ACTION_UNINSTALL_FILE
= "org.fdroid.fdroid.installer.FileInstaller.action.UNINSTALL_PACKAGE";
private FileInstallerActivity activity;
// for the broadcasts
private FileInstaller installer;
private Apk apk;
private Uri localApkUri;
private Uri downloadUri;
private int act = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = this;
Intent intent = getIntent();
String action = intent.getAction();
localApkUri = intent.getData();
downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI);
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
installer = new FileInstaller(this, apk);
if (ACTION_INSTALL_FILE.equals(action)) {
if (hasStoragePermission()) {
installPackage(localApkUri, downloadUri, apk);
} else {
requestPermission();
act = 1;
}
} else if (ACTION_UNINSTALL_FILE.equals(action)) {
if (hasStoragePermission()) {
uninstallPackage(apk);
} else {
requestPermission();
act = 2;
}
} else {
throw new IllegalStateException("Intent action not specified!");
}
}
private boolean hasStoragePermission() {
return ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
if (!hasStoragePermission()) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
showDialog();
} else {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS_REQUEST_STORAGE);
}
}
}
private void showDialog() {
// hack to get theme applied (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
final AlertDialog.Builder builder = new AlertDialog.Builder(theme);
builder.setMessage(R.string.app_permission_storage)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS_REQUEST_STORAGE);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
if (act == 1) {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED);
} else if (act == 2) {
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED);
}
finish();
}
})
.create().show();
}
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_STORAGE:
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (act == 1) {
installPackage(localApkUri, downloadUri, apk);
} else if (act == 2) {
uninstallPackage(apk);
}
} else {
if (act == 1) {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED);
} else if (act == 2) {
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED);
}
}
finish();
}
}
private void installPackage(Uri localApkUri, Uri downloadUri, Apk apk) {
Utils.debugLog(TAG, "Installing: " + localApkUri.getPath());
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED);
File path = apk.getMediaInstallPath(activity.getApplicationContext());
path.mkdirs();
try {
FileUtils.copyFileToDirectory(new File(localApkUri.getPath()), path);
} catch (IOException e) {
Utils.debugLog(TAG, "Failed to copy: " + e.getMessage());
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED);
}
if (apk.isMediaInstalled(activity.getApplicationContext())) { // Copying worked
Utils.debugLog(TAG, "Copying worked: " + localApkUri.getPath());
Toast.makeText(this, String.format(this.getString(R.string.app_installed_media), path.toString()),
Toast.LENGTH_LONG).show();
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_COMPLETE);
} else {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED);
}
finish();
}
private void uninstallPackage(Apk apk) {
if (apk.isMediaInstalled(activity.getApplicationContext())) {
File file = new File(apk.getMediaInstallPath(activity.getApplicationContext()), apk.apkName);
if (!file.delete()) {
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED);
return;
}
}
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_COMPLETE);
finish();
}
}

View File

@ -343,7 +343,7 @@ public class InstallManagerService extends Service {
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null);
Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl);
if (apkComplete != null) {
if (apkComplete != null && apkComplete.isApk()) {
try {
PackageManagerCompat.setInstaller(context, getPackageManager(), apkComplete.packageName);
} catch (SecurityException e) {

View File

@ -22,8 +22,7 @@ package org.fdroid.fdroid.installer;
import android.content.Context;
import android.text.TextUtils;
import android.widget.Toast;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
@ -47,11 +46,9 @@ public class InstallerFactory {
Installer installer;
if (apk.apkName != null && !apk.apkName.endsWith(".apk")) {
String msg = context.getString(R.string.install_error_not_yet_supported, apk.apkName);
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
Utils.debugLog(TAG, msg);
installer = new DummyInstaller(context, apk);
if (!apk.isApk()) {
Utils.debugLog(TAG, "Using FileInstaller for non-apk file");
installer = new FileInstaller(context, apk);
} else if (PrivilegedInstaller.isDefault(context)) {
Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller");
installer = new PrivilegedInstaller(context, apk);

View File

@ -478,14 +478,14 @@ public class AppDetailsRecyclerViewAdapter
if (callbacks.isAppDownloading()) {
buttonPrimaryView.setText(R.string.downloading);
buttonPrimaryView.setEnabled(false);
} else if (!app.isInstalled() && suggestedApk != null) {
} else if (!app.isInstalled(context) && suggestedApk != null) {
// Check count > 0 due to incompatible apps resulting in an empty list.
callbacks.disableAndroidBeam();
// Set Install button and hide second button
buttonPrimaryView.setText(R.string.menu_install);
buttonPrimaryView.setOnClickListener(onInstallClickListener);
buttonPrimaryView.setEnabled(true);
} else if (app.isInstalled()) {
} else if (app.isInstalled(context)) {
callbacks.enableAndroidBeam();
if (app.canAndWantToUpdate(context) && suggestedApk != null) {
buttonPrimaryView.setText(R.string.menu_upgrade);

View File

@ -322,7 +322,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
}
protected AppListItemState getViewStateReadyToInstall(@NonNull App app) {
int actionButtonLabel = app.isInstalled()
int actionButtonLabel = app.isInstalled(activity.getApplicationContext())
? R.string.app__install_downloaded_update
: R.string.menu_install;

View File

@ -35,7 +35,7 @@ public class StandardAppListItemController extends AppListItemController {
private CharSequence getStatusText(@NonNull App app) {
if (!app.compatible) {
return activity.getString(R.string.app_incompatible);
} else if (app.isInstalled()) {
} else if (app.isInstalled(activity.getApplicationContext())) {
if (app.canAndWantToUpdate(activity)) {
return activity.getString(R.string.app_version_x_available, app.getSuggestedVersionName());
} else {
@ -47,7 +47,7 @@ public class StandardAppListItemController extends AppListItemController {
}
private boolean shouldShowInstall(@NonNull App app) {
boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled();
boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled(activity.getApplicationContext());
boolean shouldAllow = app.compatible && !app.isFiltered();
return installable && shouldAllow;

View File

@ -16,9 +16,11 @@ import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.ViewGroup;
import android.widget.Toast;
import com.ashokvarma.bottomnavigation.BadgeItem;
import com.ashokvarma.bottomnavigation.BottomNavigationBar;
import com.ashokvarma.bottomnavigation.BottomNavigationItem;
import org.fdroid.fdroid.AppDetails2;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.FDroidApp;
@ -388,5 +390,4 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
}
}
};
}

View File

@ -346,7 +346,7 @@ public class SwapAppsView extends ListView implements
btnInstall.setVisibility(View.VISIBLE);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.GONE);
} else if (app.isInstalled()) {
} else if (app.isInstalled(getContext())) {
btnInstall.setVisibility(View.GONE);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.VISIBLE);

View File

@ -489,7 +489,6 @@
<item quantity="other">Kyk na alle %d</item>
</plurals>
<string name="install_error_not_yet_supported">Lêertipe kan nog nie geïnstalleer word nie: %s</string>
<string name="details_last_updated_today">Vandag opgedateer</string>
<plurals name="details_last_update_days">
<item quantity="one">%1$s dag gelede opgedateer</item>

View File

@ -460,7 +460,6 @@
<string name="repositories_summary">أضف مصادر أخرى للتطبيقات</string>
<string name="menu_license">الرخصة: %s</string>
<string name="download_404">الملف المطلوب غير موجود.</string>
<string name="install_error_not_yet_supported">نوع الملف لا يمكن بعد تثبيته: %s</string>
<string name="app__tts__downloading_progress">جاري التحميل، اكتمل %1$d%%</string>
<string name="tts_category_name">التصنيف %1$s</string>

View File

@ -460,7 +460,6 @@
<item quantity="other">Ver toles %d</item>
</plurals>
<string name="install_error_not_yet_supported">Entá nun puede instalase\'l tipu de ficheru: %s</string>
<string name="nearby_splash__download_apps_from_people_nearby">¿Nun tienes Internet? ¡Descarga apps de xente cercana!</string>
<string name="nearby_splash__find_people_button">Alcontrar persones cercanes</string>
<string name="nearby_splash__both_parties_need_fdroid">Les dos partes necesiten %1$s pa usar la cercanía.</string>

View File

@ -489,7 +489,6 @@
<string name="app__tts__cancel_download">Адмяніць спампоўку</string>
<string name="app__tts__downloading_progress">Спампоўка, %1$d%% скончана</string>
<string name="menu_license">Ліцэнзія: %s</string>
<string name="install_error_not_yet_supported">Тып файла не можа быць усталяваны: %s</string>
<plurals name="notification_summary_updates">
<item quantity="one">%1$d абнаўленне</item>
<item quantity="few">%1$d абнаўленні</item>

View File

@ -479,7 +479,6 @@
<item quantity="other">Veure\'ls tots %d</item>
</plurals>
<string name="install_error_not_yet_supported">El tipus de fitxer no es pot instal·lar: %s</string>
<string name="app__tts__downloading_progress">Descarregant, %1$d%% completat</string>
<plurals name="notification_summary_updates">
<item quantity="one">%1$d actualització</item>

View File

@ -431,7 +431,6 @@
<item quantity="other">Vis alle %d</item>
</plurals>
<string name="install_error_not_yet_supported">Kan endnu ikke installere filtypen: %s</string>
<string name="nearby_splash__download_apps_from_people_nearby">Ingen internet? Hent apps fra folk i nærheden af dig!</string>
<string name="nearby_splash__find_people_button">Find folk i nærheden af mig</string>
<string name="nearby_splash__both_parties_need_fdroid">Begge parter skal have %1$s for at benytte i nærheden.</string>

View File

@ -481,7 +481,6 @@
<string name="app__tts__cancel_download">Herunterladen abbrechen</string>
<string name="app__tts__downloading_progress">wird heruntergeladen, %1$d%% vervollständigt</string>
<string name="menu_license">Lizenz: %s</string>
<string name="install_error_not_yet_supported">Dateityp kann nicht installiert werden: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d weitere …</item>
<item quantity="other">+%1$d weitere …</item>

View File

@ -456,7 +456,6 @@
<string name="app__tts__cancel_download">Nuligi elŝutadon</string>
<string name="app__tts__downloading_progress">Elŝutado, %1$d%% kompleta</string>
<string name="menu_license">Permesilo: %s</string>
<string name="install_error_not_yet_supported">Dosier-tipo ne povas ankoraŭ esti instalita: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d pli…</item>
<item quantity="other">+1$d pli…</item>

View File

@ -473,7 +473,6 @@
<string name="app__tts__cancel_download">Cancelar descarga</string>
<string name="app__tts__downloading_progress">Descargando, %1$d%% completado</string>
<string name="menu_license">Licencia: %s</string>
<string name="install_error_not_yet_supported">No se puede instalar el tipo de fichero: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d más…</item>
<item quantity="other">+%1$d más…</item>

View File

@ -488,7 +488,6 @@
<string name="categories__empty_state__no_categories">Ez dago kategoriarik erakusteko</string>
<string name="install_error_not_yet_supported">Fitxategi mota ezin da oraindik instalatu: %s</string>
<string name="nearby_splash__download_apps_from_people_nearby">Internetik ez? Eskuratu aplikazioak inguruko jendearengandik!</string>
<string name="nearby_splash__find_people_button">Aurkitu inguruko jendea</string>
<string name="nearby_splash__both_parties_need_fdroid">Biek %1$s erabili behar dute inguruan elkar aurkitzeko.</string>

View File

@ -436,7 +436,6 @@
<item quantity="other">%1$sd روز پیش به‌روز شده‌</item>
</plurals>
<string name="menu_license">پروانه: %s</string>
<string name="install_error_not_yet_supported">این نوع فایل نمی تواند نصب شود: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d بیشتر…</item>
<item quantity="other">+%1$d بیشتر…</item>

View File

@ -496,7 +496,6 @@
<string name="details_last_updated_today">Mis à jour aujourd\'hui</string>
<string name="app__tts__cancel_download">Annuler le téléchargement</string>
<string name="menu_license">Licence: %s</string>
<string name="install_error_not_yet_supported">Ce type de fichier ne peut être installé pour l\'instant: %s</string>
<string name="app__tts__downloading_progress">Téléchargement, %1$d%% complété</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d autre…</item>

View File

@ -409,7 +409,6 @@
<item quantity="other">הצגת כל ה־%d</item>
</plurals>
<string name="install_error_not_yet_supported">עדיין לא ניתן להתקין את סוג הקובץ: %s</string>
<string name="nearby_splash__download_apps_from_people_nearby">אין חיבור לאינטרנט? ניתן להוריד יישומונים מאנשים בקרבתך!</string>
<string name="nearby_splash__find_people_button">חיפוש אנשים בקרבתי</string>
<string name="nearby_splash__both_parties_need_fdroid">שני הצדדים צריכים %1$s כדי להשתמש בקרבה.</string>

View File

@ -466,7 +466,6 @@
<string name="app__tts__cancel_download">Batalkan unduhan</string>
<string name="app__tts__downloading_progress">Mengunduh, %1$d%% selesai</string>
<string name="menu_license">Lisensi: %s</string>
<string name="install_error_not_yet_supported">Tipe berkas belum dapat dipasang: %s</string>
<plurals name="notification_summary_more">
<item quantity="other">+%1$d lainnya…</item>
</plurals>

View File

@ -502,7 +502,6 @@
<string name="antifeatureswarning">Þetta forrit er með eiginleika sem ekki er víst að þér líki við.</string>
<string name="antifeatures">Neikvæðir eiginleikar</string>
<string name="download_404">Umbeðin skrá fannst ekki.</string>
<string name="install_error_not_yet_supported">Skráartegundina er ekki enn hægt að setja upp: %s</string>
<string name="nearby_splash__download_apps_from_people_nearby">Ekkert Internet? Fáðu forrit frá fólki í nágrenninu!</string>
<string name="nearby_splash__find_people_button">Finna fólk nálægt mér</string>
<string name="nearby_splash__both_parties_need_fdroid">Báðir aðilar þurfa %1$s til að nota í nálægð.</string>

View File

@ -437,7 +437,6 @@
<string name="app__tts__cancel_download">ダウンロードをキャンセル</string>
<string name="app__tts__downloading_progress">ダウンロード中、%1$d%% 完了</string>
<string name="menu_license">ライセンス: %s</string>
<string name="install_error_not_yet_supported">ファイルの種類はまだインストールできません: %s</string>
<plurals name="notification_summary_more">
<item quantity="other">+%1$d さらに…</item>
</plurals>

View File

@ -293,7 +293,6 @@
<string name="empty_can_update_app_list">അഭിനന്ദനങ്ങൾ! നിങ്ങളുടെ പ്രയോഗങ്ങളെല്ലാം കാലികമാണ് (അല്ലെങ്കിൽ നിങ്ങളുടെ സംഭരണികള്‍ കാലഹരണപ്പെട്ടതാണ്).</string>
<string name="install_error_unknown">ഒരു അജ്ഞാത പിശക് കാരണം സ്ഥാപിക്കല്‍ പരാജയപ്പെട്ടു</string>
<string name="uninstall_error_unknown">കാരണം ഒരു അജ്ഞാത പിശക് ഒഴിവാക്കല്‍ പരാജയപ്പെട്ടു</string>
<string name="install_error_not_yet_supported">ഫയൽ തരം സ്ഥാപിക്കാന്‍ കഴിയില്ല: %s</string>
<string name="system_install_denied_signature">വിപുലീകരണത്തിന്റെ ഒപ്പ് തെറ്റാണ്! ഒരു ബഗ് റിപ്പോർട്ട് സൃഷ്ടിക്കുക!</string>
<string name="system_install_denied_permissions">പ്രത്യേക അനുമതികൾ വിപുലീകരണത്തിന് നൽകിയിട്ടില്ല! ഒരു ബഗ് റിപ്പോർട്ട് സൃഷ്ടിക്കുക!</string>
<string name="system_install_post_success">പ്രത്യേക അനുമതി ആവശ്യമുള്ള എഫ്-ഡ്രോയ്ഡ് വിപുലീകരണം സ്ഥാപിച്ചു</string>

View File

@ -479,7 +479,6 @@
<string name="app__tts__downloading_progress">Laster ned, %1$d%% fullført</string>
<string name="menu_license">Lisens: %s</string>
<string name="install_error_not_yet_supported">Filtypen kan ikke installeres enda: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d til…</item>
<item quantity="other">+%1$d til…</item>

View File

@ -450,7 +450,6 @@
<string name="app__tts__cancel_download">Annuleer download</string>
<string name="app__tts__downloading_progress">Downloaden, %1$d%% voltooid</string>
<string name="menu_license">Licentie: %s</string>
<string name="install_error_not_yet_supported">Bestandstype kan nog niet geïnstalleerd worden: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d meer…</item>
<item quantity="other">+%1$d meer…</item>

View File

@ -467,7 +467,6 @@
<string name="app__tts__cancel_download">Anuluj pobieranie</string>
<string name="app__tts__downloading_progress">Pobieranie, %1$d%% ukończono</string>
<string name="menu_license">Licencja: %s</string>
<string name="install_error_not_yet_supported">Typ pliku nie może być jeszcze zainstalowany: %s</string>
<plurals name="notification_summary_updates">
<item quantity="one">%1$d Aktualizacja</item>
<item quantity="few">%1$d Aktualizacje</item>

View File

@ -482,7 +482,6 @@
<string name="app__tts__cancel_download">Cancelar download</string>
<string name="app__tts__downloading_progress">Baixando, %1$d%% completo</string>
<string name="menu_license">Licença: %s</string>
<string name="install_error_not_yet_supported">Tipo de arquivo ainda não pode ser instalado: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d mais…</item>
<item quantity="other">+%1$d mais…</item>

View File

@ -489,7 +489,6 @@
<string name="menu_video">Vídeo</string>
<string name="menu_license">Licença: %s</string>
<string name="install_error_not_yet_supported">Este tipo de ficheiro ainda não pode ser instalado: %s</string>
<string name="app__tts__downloading_progress">A descarregar, %1$d%% completo</string>
<string name="app__tts__cancel_download">Cancelar descarga</string>
<string name="by_author_format">de %s</string>

View File

@ -438,7 +438,6 @@
<string name="app__tts__cancel_download">Anulează descărcarea</string>
<string name="menu_video">Video</string>
<string name="menu_license">Licență: %s</string>
<string name="install_error_not_yet_supported">Acest tip de fișier nu se poate încă instala: %s</string>
<string name="nearby_splash__download_apps_from_people_nearby">Nu ai acces la Internet? Descarcă aplicații de la persoanele de lângă tine!</string>
<string name="nearby_splash__find_people_button">Găsește persoane lângă mine</string>
<string name="nearby_splash__both_parties_need_fdroid">Ambele persoane au nevoie de %1$s pentru a putea folosi această opțiune.</string>

View File

@ -486,7 +486,6 @@
<string name="app__tts__cancel_download">Отменить скачивание</string>
<string name="app__tts__downloading_progress">Скачано %1$d%%</string>
<string name="menu_license">Лицензия: %s</string>
<string name="install_error_not_yet_supported">Тип файла пока не может быть установлен: %s</string>
<plurals name="notification_summary_updates">
<item quantity="one">%1$d обновление</item>
<item quantity="few">%1$d обновления</item>

View File

@ -493,7 +493,6 @@
<string name="app__tts__cancel_download">Firma s\'iscarrigamentu</string>
<string name="app__tts__downloading_progress">Iscarrighende, %1$d%% cumpridu</string>
<string name="menu_license">Litzèntzia: %s</string>
<string name="install_error_not_yet_supported">Sa casta de documentu non podet èssere installada: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d àtera…</item>
<item quantity="other">àteras +%1$d…</item>

View File

@ -462,7 +462,6 @@
<item quantity="other">Dostupných %d aplikácií</item>
</plurals>
<string name="install_error_not_yet_supported">Typ súboru zatiaľ nie je možné nainštalovať: %s</string>
<string name="nearby_splash__download_apps_from_people_nearby">Žiadny Internet? Stiahnite si aplikácie od ľudí v blízkosti vás!</string>
<string name="nearby_splash__find_people_button">Nájdi ľudí blízko mňa</string>
<string name="nearby_splash__both_parties_need_fdroid">Obidve strany potrebujú %1$s na použitie v okolí.</string>

View File

@ -486,7 +486,6 @@
<string name="app__tts__cancel_download">Откажи преузимање</string>
<string name="menu_license">Лиценца: %s</string>
<string name="app__tts__downloading_progress">Преузимам, %1$d%% завршено</string>
<string name="install_error_not_yet_supported">Тип фајла још не може бити инсталиран: %s</string>
<string name="by_author_format">од %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+још %1$d…</item>

View File

@ -466,7 +466,6 @@
<string name="details_last_updated_today">Uppdaterad idag</string>
<string name="app__tts__cancel_download">Avbryt hämtning</string>
<string name="menu_license">Licens: %s</string>
<string name="install_error_not_yet_supported">Filtypen kan inte ännu installeras: %s</string>
<string name="app__tts__downloading_progress">Hämtar, %1$d%% färdigt</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d till…</item>

View File

@ -463,7 +463,6 @@
<string name="app__tts__cancel_download">İndirmeyi iptal et</string>
<string name="app__tts__downloading_progress">İndiriliyor, %%%1$d tamamlandı</string>
<string name="menu_license">Lisans: %s</string>
<string name="install_error_not_yet_supported">Dosya türü henüz kurulamaz: %s</string>
<plurals name="notification_summary_more">
<item quantity="one">+%1$d daha…</item>
<item quantity="other">+%1$d daha…</item>

View File

@ -466,7 +466,6 @@
<item quantity="many">Усіх %d застосунків</item>
</plurals>
<string name="install_error_not_yet_supported">Цей вид файлу не може бути встановленим: %s</string>
<string name="nearby_splash__both_parties_need_fdroid">Обидві частини потребують %1$s для користання поблизу.</string>
<string name="app__tts__downloading_progress">Стягування, %1$d%% записано</string>

View File

@ -388,7 +388,6 @@
<item quantity="other">查看全部 %d</item>
</plurals>
<string name="install_error_not_yet_supported">文件类型无法安装:%s</string>
<string name="nearby_splash__download_apps_from_people_nearby">没有连上互联网?请从附近的 F-Droid 用户那里获取应用!</string>
<string name="nearby_splash__find_people_button">查找附近的人</string>
<string name="nearby_splash__both_parties_need_fdroid">双方都需要 %1$s 才可使用附近的人功能。</string>

View File

@ -448,7 +448,6 @@
<string name="app__tts__cancel_download">取消下載</string>
<string name="app__tts__downloading_progress">正在下载的 %1$d%% 完成</string>
<string name="menu_license">授權:%s</string>
<string name="install_error_not_yet_supported">還未安裝檔案類型:%s</string>
<plurals name="notification_summary_more">
<item quantity="other">+%1$d 更多…</item>
</plurals>

View File

@ -73,6 +73,8 @@
<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_installed_media">File installed to %s</string>
<string name="app_permission_storage">F-Droid needs the storage permission to install this to storage. Please allow it on the next screen to proceed with installation.</string>
<string name="app_list__name__downloading_in_progress">Downloading %1$s</string>
<string name="app_list__name__successfully_installed">%1$s installed</string>
@ -330,7 +332,6 @@
</string>
<string name="install_error_unknown">Failed to install due to an unknown error</string>
<string name="uninstall_error_unknown">Failed to uninstall due to an unknown error</string>
<string name="install_error_not_yet_supported">File type cannot yet be installed: %s</string>
<string name="system_install_denied_signature">The signature of the extension is wrong! Please create a bug
report!
</string>