Merge branch 'swap-and-installer-improvements' into 'master'
Swap and installer improvements See merge request fdroid/fdroidclient!733
This commit is contained in:
commit
795dd0dbf7
@ -161,15 +161,15 @@ dependencies {
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.8.7'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.7'
|
||||
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
|
||||
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.60'
|
||||
fullImplementation 'cc.mvdan.accesspoint:library:0.2.0'
|
||||
fullImplementation 'org.jmdns:jmdns:3.5.3'
|
||||
fullImplementation 'org.nanohttpd:nanohttpd:2.3.1'
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:3.8'
|
||||
testImplementation "com.android.support.test:monitor:1.0.2"
|
||||
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.7.22'
|
||||
|
||||
|
@ -147,10 +147,24 @@ public final class LocalRepoManager {
|
||||
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
|
||||
new FileOutputStream(indexHtml)));
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (App app : apps.values()) {
|
||||
builder.append("<li><a href=\"/fdroid/repo/")
|
||||
.append(app.installedApk.apkName)
|
||||
.append("\"><img width=\"32\" height=\"32\" src=\"/fdroid/repo/icons/")
|
||||
.append(app.packageName)
|
||||
.append("_")
|
||||
.append(app.installedApk.versionCode)
|
||||
.append(".png\">")
|
||||
.append(app.name)
|
||||
.append("</a></li>\n");
|
||||
}
|
||||
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.replaceAll("\\{\\{REPO_URL\\}\\}", repoAddress);
|
||||
line = line.replaceAll("\\{\\{CLIENT_URL\\}\\}", fdroidClientURL);
|
||||
line = line.replaceAll("\\{\\{APP_LIST\\}\\}", builder.toString());
|
||||
out.write(line);
|
||||
}
|
||||
in.close();
|
||||
|
@ -257,7 +257,9 @@ public class StartSwapView extends RelativeLayout implements SwapWorkflowActivit
|
||||
textBluetoothVisible.setText(textResource);
|
||||
|
||||
bluetoothSwitch = (SwitchCompat) findViewById(R.id.switch_bluetooth);
|
||||
Utils.debugLog(TAG, getManager().isBluetoothDiscoverable() ? "Initially marking switch as checked, because Bluetooth is discoverable." : "Initially marking switch as not-checked, because Bluetooth is not discoverable.");
|
||||
Utils.debugLog(TAG, getManager().isBluetoothDiscoverable()
|
||||
? "Initially marking switch as checked, because Bluetooth is discoverable."
|
||||
: "Initially marking switch as not-checked, because Bluetooth is not discoverable.");
|
||||
bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
|
||||
setBluetoothSwitchState(getManager().isBluetoothDiscoverable(), true);
|
||||
|
||||
|
@ -129,7 +129,7 @@
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/install_history_file_provider"/>
|
||||
android:resource="@xml/installer_file_provider"/>
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
|
@ -71,6 +71,28 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
details {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul > li {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
ul > li a {
|
||||
font-size: xx-large;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
ul > li a img {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
#download-from-web {
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
@ -105,5 +127,12 @@
|
||||
<img src="swap-tick-not-done.png" class="tick not-done" alt="Not done" />
|
||||
</li>
|
||||
</ol>
|
||||
<br/><br/><br/><br/>
|
||||
<details>
|
||||
<summary>Available Apps</summary>
|
||||
<ul>
|
||||
{{APP_LIST}}
|
||||
</ul>
|
||||
</details>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -580,7 +580,6 @@ public class FDroidApp extends Application {
|
||||
|
||||
String bluetoothPackageName = null;
|
||||
String className = null;
|
||||
boolean found = false;
|
||||
Intent sendBt = null;
|
||||
|
||||
try {
|
||||
@ -599,20 +598,19 @@ public class FDroidApp extends Application {
|
||||
if ("com.android.bluetooth".equals(bluetoothPackageName)
|
||||
|| "com.mediatek.bluetooth".equals(bluetoothPackageName)) {
|
||||
className = info.activityInfo.name;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "Could not get application info to send via bluetooth", e);
|
||||
found = false;
|
||||
className = null;
|
||||
} catch (IOException e) {
|
||||
Exception toLog = new RuntimeException("Error preparing file to send via Bluetooth", e);
|
||||
ACRA.getErrorReporter().handleException(toLog, false);
|
||||
}
|
||||
|
||||
if (sendBt != null) {
|
||||
if (found) {
|
||||
if (className != null) {
|
||||
sendBt.setClassName(bluetoothPackageName, className);
|
||||
activity.startActivity(sendBt);
|
||||
} else {
|
||||
|
@ -37,14 +37,18 @@ import java.io.IOException;
|
||||
* either locally or for sending via bluetooth.
|
||||
* <p/>
|
||||
* APK handling for installations:
|
||||
* 1. APKs are downloaded into a cache directory that is either created on SD card
|
||||
* <ol>
|
||||
* <li>APKs are downloaded into a cache directory that is either created on SD card
|
||||
* <i>"/Android/data/[app_package_name]/cache/apks"</i> (if card is mounted and app has
|
||||
* appropriate permission) or on device's file system depending incoming parameters.
|
||||
* 2. Before installation, the APK is copied into the private data directory of the F-Droid,
|
||||
* <i>"/data/data/[app_package_name]/files/install-$random.apk"</i>.
|
||||
* 3. The hash of the file is checked against the expected hash from the repository
|
||||
* 4. For Android < 7, a file Uri pointing to the File is returned, for Android >= 7,
|
||||
* a content Uri is returned using support lib's FileProvider.
|
||||
* appropriate permission) or on device's file system depending incoming parameters</li>
|
||||
* <li>Before installation, the APK is copied into the private data directory of the F-Droid,
|
||||
* <i>"/data/data/[app_package_name]/files/install-$random.apk"</i></li>
|
||||
* <li>The hash of the file is checked against the expected hash from the repository</li>
|
||||
* <li>For {@link Build.VERSION_CODES#M < android-23}, a {@code file://} {@link Uri}
|
||||
* pointing to the {@link File} is returned, for {@link Build.VERSION_CODES#M >= android-23},
|
||||
* a {@code content://} {@code Uri} is returned using support lib's
|
||||
* {@link FileProvider}</li>
|
||||
* </ol>
|
||||
*/
|
||||
public class ApkFileProvider extends FileProvider {
|
||||
|
||||
@ -52,7 +56,7 @@ public class ApkFileProvider extends FileProvider {
|
||||
|
||||
public static Uri getSafeUri(Context context, PackageInfo packageInfo) throws IOException {
|
||||
SanitizedFile tempApkFile = ApkCache.copyInstalledApkToFiles(context, packageInfo);
|
||||
return getSafeUri(context, tempApkFile, Build.VERSION.SDK_INT >= 24);
|
||||
return getSafeUri(context, tempApkFile, Build.VERSION.SDK_INT >= 23);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,12 +93,12 @@ public class ApkFileProvider extends FileProvider {
|
||||
context.grantUriPermission(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME,
|
||||
apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
context.grantUriPermission("com.android.bluetooth", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
context.grantUriPermission("com.mediatek.bluetooth", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
return apkUri;
|
||||
} else {
|
||||
tempFile.setReadable(true, false);
|
||||
return Uri.fromFile(tempFile);
|
||||
}
|
||||
|
||||
tempFile.setReadable(true, false);
|
||||
|
||||
return Uri.fromFile(tempFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ import android.net.Uri;
|
||||
import android.os.Process;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
||||
@ -47,8 +46,7 @@ import java.util.List;
|
||||
public class InstallHistoryService extends IntentService {
|
||||
public static final String TAG = "InstallHistoryService";
|
||||
|
||||
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".installer";
|
||||
public static final Uri LOG_URI = Uri.parse("content://" + AUTHORITY + "/install_history/all");
|
||||
public static final Uri LOG_URI = Uri.parse("content://" + Installer.AUTHORITY + "/install_history/all");
|
||||
|
||||
private static BroadcastReceiver broadcastReceiver;
|
||||
|
||||
|
@ -31,6 +31,7 @@ import android.os.PatternMatcher;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
@ -51,6 +52,8 @@ public abstract class Installer {
|
||||
final Context context;
|
||||
final Apk apk;
|
||||
|
||||
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".installer";
|
||||
|
||||
public static final String ACTION_INSTALL_STARTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_STARTED";
|
||||
public static final String ACTION_INSTALL_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.INSTALL_COMPLETE";
|
||||
public static final String ACTION_INSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_INTERRUPTED";
|
||||
|
@ -7,10 +7,12 @@ import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.content.FileProvider;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.widget.TextViewCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
@ -28,6 +30,7 @@ import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
@ -35,6 +38,7 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
@ -43,10 +47,12 @@ import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.privileged.views.AppDiff;
|
||||
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
|
||||
import org.fdroid.fdroid.views.main.MainActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -512,9 +518,42 @@ public class AppDetailsRecyclerViewAdapter
|
||||
buttonPrimaryView.setText(R.string.menu_upgrade);
|
||||
buttonPrimaryView.setOnClickListener(onUpgradeClickListener);
|
||||
} else {
|
||||
Apk mediaApk = app.getMediaApkifInstalled(context);
|
||||
if (context.getPackageManager().getLaunchIntentForPackage(app.packageName) != null) {
|
||||
buttonPrimaryView.setText(R.string.menu_launch);
|
||||
buttonPrimaryView.setOnClickListener(onLaunchClickListener);
|
||||
} else if (!app.isApk && mediaApk != null) {
|
||||
final File installedFile = new File(mediaApk.getMediaInstallPath(context), mediaApk.apkName);
|
||||
if (!installedFile.toString().startsWith(context.getApplicationInfo().dataDir)) {
|
||||
final Intent viewIntent = new Intent(Intent.ACTION_VIEW);
|
||||
Uri uri = FileProvider.getUriForFile(context, Installer.AUTHORITY, installedFile);
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
FilenameUtils.getExtension(installedFile.getName()));
|
||||
viewIntent.setDataAndType(uri, mimeType);
|
||||
if (Build.VERSION.SDK_INT < 19) {
|
||||
viewIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
} else {
|
||||
viewIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
}
|
||||
if (context.getPackageManager().queryIntentActivities(viewIntent, 0).size() > 0) {
|
||||
buttonPrimaryView.setText(R.string.menu_open);
|
||||
buttonPrimaryView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
try {
|
||||
context.startActivity(viewIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
buttonPrimaryView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
buttonPrimaryView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
buttonPrimaryView.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
@ -58,4 +57,13 @@ class InstalledAppListAdapter extends RecyclerView.Adapter<InstalledAppListItemC
|
||||
this.cursor = cursor;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public App getItem(int position) {
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
cursor.moveToPosition(position);
|
||||
return new App(cursor);
|
||||
}
|
||||
}
|
||||
|
@ -22,17 +22,20 @@ package org.fdroid.fdroid.views.installed;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.ShareCompat;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
|
||||
@ -100,4 +103,35 @@ public class InstalledAppsActivity extends AppCompatActivity implements LoaderMa
|
||||
adapter.setApps(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.installed_apps, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_share:
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.append("packageName,versionCode,versionName\n");
|
||||
for (int i = 0; i < adapter.getItemCount(); i++) {
|
||||
App app = adapter.getItem(i);
|
||||
if (app != null) {
|
||||
stringBuilder.append(app.packageName).append(',')
|
||||
.append(app.installedVersionCode).append(',')
|
||||
.append(app.installedVersionName).append('\n');
|
||||
}
|
||||
}
|
||||
ShareCompat.IntentBuilder intentBuilder = ShareCompat.IntentBuilder.from(this)
|
||||
.setSubject(getString(R.string.send_installed_apps))
|
||||
.setChooserTitle(R.string.send_installed_apps)
|
||||
.setText(stringBuilder.toString())
|
||||
.setType("text/csv");
|
||||
startActivity(intentBuilder.getIntent());
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
9
app/src/main/res/menu/installed_apps.xml
Normal file
9
app/src/main/res/menu/installed_apps.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/menu_share"
|
||||
android:icon="@drawable/ic_share_white"
|
||||
android:title="@string/menu_share"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
</menu>
|
@ -110,6 +110,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="app_list__dismiss_downloading_app">Download canceled</string>
|
||||
|
||||
<string name="installed_apps__activity_title">Installed Apps</string>
|
||||
<string name="send_installed_apps">Share installed apps</string>
|
||||
<string name="send_installed_apps_csv">Apps installed by F-Droid as CSV file</string>
|
||||
<string name="installed_app__updates_ignored">Updates ignored</string>
|
||||
<string name="installed_app__updates_ignored_for_suggested_version">Updates ignored for Version %1$s</string>
|
||||
<!-- The inline download button shown in the "Updates" screen only uses an icon and so requires
|
||||
@ -171,6 +173,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="menu_add_repo">New Repository</string>
|
||||
|
||||
<string name="menu_launch">Run</string>
|
||||
<string name="menu_open">Open</string>
|
||||
<string name="menu_share">Share</string>
|
||||
<string name="menu_install">Install</string>
|
||||
<string name="menu_uninstall">Uninstall</string>
|
||||
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path
|
||||
name="install_history"
|
||||
path="install_history" />
|
||||
</paths>
|
5
app/src/main/res/xml/installer_file_provider.xml
Normal file
5
app/src/main/res/xml/installer_file_provider.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="install_history" path="install_history"/>
|
||||
<external-path name="external" path="/"/>
|
||||
</paths>
|
Loading…
x
Reference in New Issue
Block a user