Merge branch 'non-apk' into 'master'
Add support for non-apk files Closes #1023 See merge request !541
This commit is contained in:
		
						commit
						a430c17950
					
				@ -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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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), SanitizedFile.sanitizeFileName(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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 installedVersionCode > 0 || isMediaInstalled(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1023,7 +1023,7 @@ public class AppProvider extends FDroidProvider {
 | 
			
		||||
                        "   LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " +
 | 
			
		||||
                " WHERE " +
 | 
			
		||||
                    app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " +
 | 
			
		||||
                    apk + "." + ApkTable.Cols.SIGNATURE + " = COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
 | 
			
		||||
                    apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
 | 
			
		||||
                    restrictToStable +
 | 
			
		||||
                    " ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + Cols.IS_COMPATIBLE + " = 1 ) ) " +
 | 
			
		||||
                " WHERE " + Cols.UPSTREAM_VERSION_CODE + " > 0 " + restrictToApp;
 | 
			
		||||
@ -1069,7 +1069,7 @@ public class AppProvider extends FDroidProvider {
 | 
			
		||||
                "   LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " +
 | 
			
		||||
                " WHERE " +
 | 
			
		||||
                    app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " +
 | 
			
		||||
                    apk + "." + ApkTable.Cols.SIGNATURE + " = COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
 | 
			
		||||
                    apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
 | 
			
		||||
                    " ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + " = 1 ) ) " +
 | 
			
		||||
                " WHERE " + restrictToApps;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ public class SanitizedFile extends File {
 | 
			
		||||
     * Removes anything that is not an alpha numeric character, or one of "-", ".", or "_".
 | 
			
		||||
     */
 | 
			
		||||
    public static String sanitizeFileName(String name) {
 | 
			
		||||
        return name.replaceAll("[^A-Za-z0-9-._]", "");
 | 
			
		||||
        return name.replaceAll("[^A-Za-z0-9-._ ]", "");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -37,11 +37,11 @@ public class SanitizedFileTest {
 | 
			
		||||
 | 
			
		||||
        assertEquals("/tmp/blah/safe", safeSanitized.getAbsolutePath());
 | 
			
		||||
        assertEquals("/tmp/blah/safe-and_bleh.boo", nonEvilSanitized.getAbsolutePath());
 | 
			
		||||
        assertEquals("/tmp/blah/rmetcshadow", evilSanitized.getAbsolutePath());
 | 
			
		||||
        assertEquals("/tmp/blah/rm etcshadow", evilSanitized.getAbsolutePath());
 | 
			
		||||
 | 
			
		||||
        assertEquals("safe", safeSanitized.getName());
 | 
			
		||||
        assertEquals("safe-and_bleh.boo", nonEvilSanitized.getName());
 | 
			
		||||
        assertEquals("rmetcshadow", evilSanitized.getName());
 | 
			
		||||
        assertEquals("rm etcshadow", evilSanitized.getName());
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user