Redesign PrivilegedInstaller

* use new local broadcasts
* show permission screen before download
* display permission screen as dialog
This commit is contained in:
Dominik Schürmann 2016-05-29 23:34:00 +03:00
parent 592cd0424a
commit 2776b86050
19 changed files with 517 additions and 272 deletions

View File

@ -316,12 +316,18 @@
<activity
android:name=".privileged.views.InstallConfirmActivity"
android:label="@string/menu_install"
android:theme="@style/MinWithDialogBaseThemeLight"
android:excludeFromRecents="true"
android:parentActivityName=".FDroid"
android:configChanges="layoutDirection|locale" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<activity
android:name=".privileged.views.UninstallDialogActivity"
android:excludeFromRecents="true"
android:theme="@style/AppThemeTransparent" />
<activity
android:name=".views.ManageReposActivity"
android:label="@string/app_name"

View File

@ -79,7 +79,6 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.Utils.CommaSeparatedList;
import org.fdroid.fdroid.compat.PackageManagerCompat;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
@ -88,6 +87,7 @@ import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.InstallerFactory;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
@ -101,6 +101,8 @@ public class AppDetails extends AppCompatActivity {
private static final String TAG = "AppDetails";
private static final int REQUEST_ENABLE_BLUETOOTH = 2;
private static final int REQUEST_PERMISSION_DIALOG = 3;
private static final int REQUEST_UNINSTALL_DIALOG = 4;
public static final String EXTRA_APPID = "appid";
public static final String EXTRA_FROM = "from";
@ -983,6 +985,19 @@ public class AppDetails extends AppCompatActivity {
}
private void initiateInstall(Apk apk) {
Installer installer = InstallerFactory.create(this);
Intent intent = installer.getPermissionScreen(apk);
if (intent != null) {
// permission screen required
Utils.debugLog(TAG, "permission screen required");
startActivityForResult(intent, REQUEST_PERMISSION_DIALOG);
return;
}
startInstall(apk);
}
private void startInstall(Apk apk) {
activeDownloadUrlString = apk.getUrl();
registerDownloaderReceivers();
headerFragment.startProgress();
@ -990,9 +1005,22 @@ public class AppDetails extends AppCompatActivity {
}
private void uninstallApk(String packageName) {
Installer installer = InstallerFactory.create(this);
Intent intent = installer.getUninstallScreen(packageName);
if (intent != null) {
// uninstall screen required
Utils.debugLog(TAG, "screen screen required");
startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
return;
}
startUninstall();
}
private void startUninstall() {
localBroadcastManager.registerReceiver(uninstallReceiver,
Installer.getUninstallIntentFilter(packageName));
InstallerService.uninstall(context, packageName);
Installer.getUninstallIntentFilter(app.packageName));
InstallerService.uninstall(context, app.packageName);
}
private void launchApk(String packageName) {
@ -1016,6 +1044,18 @@ public class AppDetails extends AppCompatActivity {
case REQUEST_ENABLE_BLUETOOTH:
fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
break;
case REQUEST_PERMISSION_DIALOG:
if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
Apk apk = ApkProvider.Helper.find(this, uri, ApkProvider.DataColumns.ALL);
startInstall(apk);
}
break;
case REQUEST_UNINSTALL_DIALOG:
if (resultCode == Activity.RESULT_OK) {
startUninstall();
}
break;
}
}

View File

@ -125,6 +125,23 @@ public class FDroidApp extends Application {
}
}
public void applyDialogTheme(Activity activity) {
activity.setTheme(getCurDialogThemeResId());
}
public static int getCurDialogThemeResId() {
switch (curTheme) {
case light:
return R.style.MinWithDialogBaseThemeLight;
case dark:
return R.style.MinWithDialogBaseThemeDark;
case night:
return R.style.MinWithDialogBaseThemeDark;
default:
return R.style.MinWithDialogBaseThemeLight;
}
}
public static void enableSpongyCastle() {
Security.addProvider(SPONGYCASTLE_PROVIDER);
}

View File

@ -57,6 +57,7 @@ import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.Iterator;
@ -457,6 +458,19 @@ public final class Utils {
return splitter.iterator();
}
public ArrayList<String> toArrayList() {
ArrayList<String> out = new ArrayList<>();
for (String element : this) {
out.add(element);
}
return out;
}
public String[] toArray() {
ArrayList<String> list = toArrayList();
return list.toArray(new String[list.size()]);
}
public boolean contains(String v) {
for (final String s : this) {
if (s.equals(v)) {

View File

@ -107,8 +107,12 @@ public class ApkProvider extends FDroidProvider {
}
public static Apk find(Context context, String packageName, int versionCode, String[] projection) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getContentUri(packageName, versionCode);
return find(context, uri, projection);
}
public static Apk find(Context context, Uri uri, String[] projection) {
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(uri, projection, null, null, null);
Apk apk = null;
if (cursor != null) {

View File

@ -85,4 +85,9 @@ public class DefaultInstaller extends Installer {
sendBroadcastUninstall(packageName,
Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
}
@Override
protected boolean isUnattended() {
return false;
}
}

View File

@ -96,4 +96,9 @@ public class ExtensionInstaller extends Installer {
// don't use broadcasts for the rest of this special installer
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_COMPLETE);
}
@Override
protected boolean isUnattended() {
return false;
}
}

View File

@ -37,6 +37,8 @@ import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.privileged.views.AppDiff;
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
import org.fdroid.fdroid.privileged.views.InstallConfirmActivity;
import org.fdroid.fdroid.privileged.views.UninstallDialogActivity;
import java.io.File;
import java.io.IOException;
@ -153,48 +155,55 @@ public abstract class Installer {
return Uri.fromFile(sanitizedApkFile);
}
public PendingIntent getPermissionScreen(Apk apk) {
// old code:
// Uri packageUri = Uri.fromFile(apkFile);
// int count = newPermissionCount(packageUri);
// if (count < 0) {
// mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_CANNOT_PARSE);
public Intent getPermissionScreen(Apk apk) {
if (!isUnattended()) {
return null;
}
// install_error_cannot_parse
int count = newPermissionCount(apk);
if (count > 0) {
Uri uri = ApkProvider.getContentUri(apk);
Intent intent = new Intent(mContext, InstallConfirmActivity.class);
intent.setData(uri);
// return;
// }
// if (count > 0) {
// Intent intent = new Intent(mContext, InstallConfirmActivity.class);
// intent.setData(packageUri);
// mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS);
// } else {
// try {
// doInstallPackageInternal(packageUri);
// } catch (InstallFailedException e) {
// mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_OTHER);
// }
// }
return null;
return intent;
} else {
// no permission screen needed!
return null;
}
}
private int newPermissionCount(Apk apk) {
// TODO: requires targetSdk in Apk class/database
// boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion
// >= Build.VERSION_CODES.M;
// if (supportsRuntimePermissions) {
// return 0;
// }
private int newPermissionCount(Uri packageUri) {
AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri);
AppDiff appDiff = new AppDiff(mContext.getPackageManager(), apk);
if (appDiff.mPkgInfo == null) {
// could not get diff because we couldn't parse the package
return -1;
throw new RuntimeException("cannot parse!");
}
AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.mPkgInfo);
if (appDiff.mInstalledAppInfo != null) {
// update to an existing app
return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW);
}
// default: even if there aren't any permissions, we want to make the
// user always confirm installing new apps
return 1;
// new app install
return perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
}
public Intent getUninstallScreen(String packageName) {
if (!isUnattended()) {
return null;
}
Intent intent = new Intent(mContext, UninstallDialogActivity.class);
intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
return intent;
}
/**
@ -287,4 +296,6 @@ public abstract class Installer {
protected abstract void uninstallPackage(String packageName);
protected abstract boolean isUnattended();
}

View File

@ -238,7 +238,7 @@ public class PrivilegedInstaller extends Installer {
private static final HashMap<Integer, String> sUninstallReturnCodes;
static {
// Descriptions extrgacted from the source code comments in AOSP
// Descriptions extracted from the source code comments in AOSP
sUninstallReturnCodes = new HashMap<>();
sUninstallReturnCodes.put(DELETE_SUCCEEDED,
"Success");
@ -324,6 +324,7 @@ public class PrivilegedInstaller extends Installer {
@Override
protected void installPackage(final Uri uri, final Uri originatingUri, String packageName) {
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
final Uri sanitizedUri;
try {
@ -374,6 +375,8 @@ public class PrivilegedInstaller extends Installer {
@Override
protected void uninstallPackage(final String packageName) {
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
ApplicationInfo appInfo;
try {
//noinspection WrongConstant (lint is actually wrong here!)
@ -466,33 +469,9 @@ public class PrivilegedInstaller extends Installer {
Context.BIND_AUTO_CREATE);
}
// @Override
// public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
// switch (requestCode) {
// case REQUEST_CONFIRM_PERMS:
// if (resultCode == Activity.RESULT_OK) {
// final Uri packageUri = data.getData();
// try {
// doInstallPackageInternal(packageUri);
// } catch (InstallFailedException e) {
// mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_OTHER);
// }
// } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) {
// mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_CANNOT_PARSE);
// install_error_cannot_parse
// } else { // Activity.RESULT_CANCELED
// mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_CANCELED);
// }
// return true;
// default:
// return false;
// }
// }
@Override
protected boolean isUnattended() {
return true;
}
}

View File

@ -18,11 +18,18 @@
package org.fdroid.fdroid.privileged.views;
import android.annotation.TargetApi;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import org.fdroid.fdroid.data.Apk;
import java.util.ArrayList;
@TargetApi(Build.VERSION_CODES.M)
public class AppDiff {
private final PackageManager mPm;
@ -30,6 +37,29 @@ public class AppDiff {
public ApplicationInfo mInstalledAppInfo;
/**
* Constructor based on F-Droids Apk object
*/
public AppDiff(PackageManager mPm, Apk apk) {
this.mPm = mPm;
if (apk.permissions == null) {
throw new RuntimeException("apk.permissions is null");
}
mPkgInfo = new PackageInfo();
mPkgInfo.packageName = apk.packageName;
mPkgInfo.applicationInfo = new ApplicationInfo();
// TODO: duplicate code with Permission.fdroidToAndroid
ArrayList<String> permissionsFixed = new ArrayList<>();
for (String perm : apk.permissions.toArrayList()) {
permissionsFixed.add("android.permission." + perm);
}
mPkgInfo.requestedPermissions = permissionsFixed.toArray(new String[permissionsFixed.size()]);
init();
}
public AppDiff(PackageManager mPm, Uri mPackageURI) {
this.mPm = mPm;
@ -55,7 +85,7 @@ public class AppDiff {
String pkgName = mPkgInfo.packageName;
// Check if there is already a package on the device with this name
// but it has been renamed to something else.
final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] {pkgName});
final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[]{pkgName});
if (oldName != null && oldName.length > 0 && oldName[0] != null) {
pkgName = oldName[0];
mPkgInfo.packageName = pkgName;

View File

@ -235,8 +235,7 @@ public class AppSecurityPermissions {
try {
installedPkgInfo = mPm.getPackageInfo(info.packageName,
PackageManager.GET_PERMISSIONS);
} catch (NameNotFoundException e) {
throw new RuntimeException("NameNotFoundException during GET_PERMISSIONS!");
} catch (NameNotFoundException ignored) {
}
extractPerms(info, permSet, installedPkgInfo);
}

View File

@ -18,16 +18,16 @@
package org.fdroid.fdroid.privileged.views;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
@ -38,15 +38,23 @@ import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
/**
* NOTES:
* Parts are based on AOSP src/com/android/packageinstaller/PackageInstallerActivity.java
* latest included commit: c23d802958158d522e7350321ad9ac6d43013883
*/
public class InstallConfirmActivity extends Activity implements OnCancelListener, OnClickListener {
public class InstallConfirmActivity extends FragmentActivity implements OnCancelListener, OnClickListener {
public static final int RESULT_CANNOT_PARSE = RESULT_FIRST_USER + 1;
@ -67,16 +75,27 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
private static final String TAB_ID_ALL = "all";
private static final String TAB_ID_NEW = "new";
private App mApp;
private final DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.imageScaleType(ImageScaleType.NONE)
.showImageOnLoading(R.drawable.ic_repo_app_default)
.showImageForEmptyUri(R.drawable.ic_repo_app_default)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
private void startInstallConfirm() {
final Drawable appIcon = mAppDiff.mPkgInfo.applicationInfo.loadIcon(mPm);
final String appLabel = (String) mAppDiff.mPkgInfo.applicationInfo.loadLabel(mPm);
View appSnippet = findViewById(R.id.app_snippet);
((ImageView) appSnippet.findViewById(R.id.app_icon)).setImageDrawable(appIcon);
((TextView) appSnippet.findViewById(R.id.app_name)).setText(appLabel);
TextView appName = (TextView) appSnippet.findViewById(R.id.app_name);
ImageView appIcon = (ImageView) appSnippet.findViewById(R.id.app_icon);
TabHost tabHost = (TabHost) findViewById(android.R.id.tabhost);
appName.setText(mApp.name);
ImageLoader.getInstance().displayImage(mApp.iconUrlLarge, appIcon,
displayImageOptions);
tabHost.setup();
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);
@ -136,7 +155,7 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
: R.string.install_confirm_update_no_perms;
} else {
// This is a new application with no permissions.
msg = R.string.install_confirm_no_perms;
throw new RuntimeException("no permissions requested. This screen should not appear!");
}
tabHost.setVisibility(View.GONE);
findViewById(R.id.filler).setVisibility(View.VISIBLE);
@ -171,20 +190,28 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
((FDroidApp) getApplication()).applyTheme(this);
((FDroidApp) getApplication()).applyDialogTheme(this);
mPm = getPackageManager();
intent = getIntent();
Uri packageURI = intent.getData();
Uri uri = intent.getData();
Apk apk = ApkProvider.Helper.find(this, uri, ApkProvider.DataColumns.ALL);
mApp = AppProvider.Helper.findByPackageName(getContentResolver(), apk.packageName);
mAppDiff = new AppDiff(mPm, packageURI);
mAppDiff = new AppDiff(mPm, apk);
if (mAppDiff.mPkgInfo == null) {
setResult(RESULT_CANNOT_PARSE, intent);
finish();
}
setContentView(R.layout.install_start);
// increase dialog to full width for now
// TODO: create a better design and minimum width for tablets
getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
mInstallConfirm = findViewById(R.id.install_confirm_panel);
mInstallConfirm.setVisibility(View.INVISIBLE);

View File

@ -0,0 +1,106 @@
/*
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.privileged.views;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AlertDialog;
import android.view.ContextThemeWrapper;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.installer.Installer;
public class UninstallDialogActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
final String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
PackageManager pm = getPackageManager();
ApplicationInfo appInfo;
try {
//noinspection WrongConstant (lint is actually wrong here!)
appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Failed to get ApplicationInfo for uninstalling");
}
final boolean isSystem = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
final boolean isUpdate = (appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
if (isSystem && !isUpdate) {
// Cannot remove system apps unless we're uninstalling updates
throw new RuntimeException("Cannot remove system apps unless we're uninstalling updates");
}
int messageId;
if (isUpdate) {
messageId = R.string.uninstall_update_confirm;
} else {
messageId = R.string.uninstall_confirm;
}
// 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.setTitle(appInfo.loadLabel(pm));
builder.setIcon(appInfo.loadIcon(pm));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent data = new Intent();
data.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
setResult(Activity.RESULT_OK, intent);
finish();
}
});
builder.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
setResult(Activity.RESULT_CANCELED);
finish();
}
});
builder.setOnCancelListener(
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
setResult(Activity.RESULT_CANCELED);
finish();
}
});
builder.setMessage(messageId);
builder.create().show();
}
}

View File

@ -21,37 +21,35 @@
user before it is installed.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/install_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/install_confirm"
android:textAppearance="?android:attr/textAppearanceMedium"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dip" />
android:id="@+id/install_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dip"
android:text="@string/install_confirm"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?android:attr/dividerHorizontal"
android:visibility="gone" />
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?android:attr/dividerHorizontal"
android:visibility="gone" />
<FrameLayout
android:id="@+id/filler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone">
</FrameLayout>
android:visibility="gone"></FrameLayout>
<TabHost
android:id="@android:id/tabhost"
@ -60,24 +58,28 @@
android:layout_weight="1">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<HorizontalScrollView android:id="@+id/tabscontainer"
<HorizontalScrollView
android:id="@+id/tabscontainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/tab_unselected_holo"
android:fillViewport="true"
android:scrollbars="none">
<FrameLayout android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TabWidget
android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
android:layout_gravity="center"
android:orientation="horizontal" />
</FrameLayout>
</HorizontalScrollView>
@ -85,64 +87,68 @@
android:id="@android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
android:layout_weight="0" />
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
android:layout_weight="1" />
</LinearLayout>
</TabHost>
<!-- OK confirm and cancel buttons. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:divider="?android:attr/dividerHorizontal"
android:showDividers="beginning">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="?android:attr/dividerHorizontal"
android:orientation="vertical"
android:showDividers="beginning">
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/leftSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:measureWithLargestChild="true">
android:visibility="gone" />
<LinearLayout android:id="@+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<Button
android:id="@+id/cancel_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:maxLines="2"
android:text="@string/cancel" />
<Button android:id="@+id/cancel_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:text="@string/cancel"
android:maxLines="2"
style="?android:attr/buttonBarButtonStyle" />
<Button
android:id="@+id/ok_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:filterTouchesWhenObscured="true"
android:maxLines="2"
android:text="@string/next" />
<Button android:id="@+id/ok_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:text="@string/next"
android:maxLines="2"
android:filterTouchesWhenObscured="true"
style="?android:attr/buttonBarButtonStyle" />
<LinearLayout android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<LinearLayout
android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,59 +16,46 @@
<!--
Defines the layout of the application snippet that appears on top of the
installation screens
-->
<!-- The snippet about the application - title, icon, description. -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
--><!-- The snippet about the application - title, icon, description. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/app_snippet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dip"
android:paddingStart="16dip"
android:paddingRight="16dip"
android:paddingEnd="16dip"
android:paddingTop="24dip"
>
<ImageView android:id="@+id/app_icon"
android:layout_width="32dip"
android:layout_height="32dip"
android:paddingLeft="16dip"
android:paddingRight="16dip"
android:paddingStart="16dip"
android:paddingTop="16dip">
<ImageView
android:id="@+id/app_icon"
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_marginLeft="8dip"
android:layout_marginStart="8dip"
android:background="@android:color/transparent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:gravity="start"
android:scaleType="centerCrop"/>
<TextView android:id="@+id/app_name"
android:scaleType="centerCrop"
tools:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:ellipsize="end"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary"
android:paddingEnd="16dip"
android:paddingLeft="16dip"
android:paddingRight="16dip"
android:paddingStart="16dip"
android:shadowColor="@color/shadow"
android:shadowRadius="2"
android:layout_toRightOf="@id/app_icon"
android:layout_toEndOf="@id/app_icon"
android:singleLine="true"
android:layout_centerInParent="true"
android:paddingRight="16dip"
android:paddingEnd="16dip"
android:paddingTop="3dip"
android:paddingLeft="16dip"
android:paddingStart="16dip"
android:ellipsize="end"/>
<FrameLayout
android:id="@+id/top_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dip"
android:layout_below="@id/app_name">
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary"
tools:text="App Name" />
</RelativeLayout>
</LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,36 +20,34 @@
user before it is installed.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/install_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/install_confirm"
android:textAppearance="?android:attr/textAppearanceMedium"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dip" />
android:id="@+id/install_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dip"
android:text="@string/install_confirm"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone" />
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/filler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone">
</FrameLayout>
android:visibility="gone"></FrameLayout>
<TabHost
android:id="@android:id/tabhost"
@ -59,24 +56,28 @@
android:layout_weight="1">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<HorizontalScrollView android:id="@+id/tabscontainer"
<HorizontalScrollView
android:id="@+id/tabscontainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/tab_unselected_holo"
android:fillViewport="true"
android:scrollbars="none">
<FrameLayout android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TabWidget
android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
android:layout_gravity="center"
android:orientation="horizontal" />
</FrameLayout>
</HorizontalScrollView>
@ -84,63 +85,65 @@
android:id="@android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
android:layout_weight="0" />
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
android:layout_weight="1" />
</LinearLayout>
</TabHost>
<!-- OK confirm and cancel buttons. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:divider="?android:attr/dividerHorizontal"
android:showDividers="beginning">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="?android:attr/dividerHorizontal"
android:orientation="vertical"
android:showDividers="beginning">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/leftSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:measureWithLargestChild="true">
android:visibility="gone" />
<LinearLayout android:id="@+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<Button
android:id="@+id/cancel_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:maxLines="2"
android:text="@string/cancel" />
<Button android:id="@+id/cancel_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:text="@string/cancel"
android:maxLines="2"
/>
<Button
android:id="@+id/ok_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:filterTouchesWhenObscured="true"
android:maxLines="2"
android:text="@string/next" />
<Button android:id="@+id/ok_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:text="@string/next"
android:maxLines="2"
android:filterTouchesWhenObscured="true"
/>
<LinearLayout android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<LinearLayout
android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,24 +13,22 @@
limitations under the License.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content">
<include
android:id="@+id/app_snippet"
layout="@layout/install_app_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/app_snippet"/>
android:layout_height="wrap_content" />
<include
layout="@layout/install_confirm"
android:id="@+id/install_confirm_panel"
layout="@layout/install_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/app_snippet"
android:layout_alignParentBottom="true"/>
android:layout_below="@id/app_snippet" />
</RelativeLayout>

View File

@ -268,7 +268,6 @@
<string name="root_access_denied_title">Root access denied</string>
<string name="root_access_denied_body">Either your Android device is not rooted or you have denied root access for F-Droid.</string>
<string name="install_error_unknown">Failed to install due to an unknown error</string>
<string name="install_error_cannot_parse">An error occurred while parsing the package</string>
<string name="uninstall_error_unknown">Failed to uninstall due to an unknown error</string>
<string name="system_install_denied_title">F-Droid Privileged Extension is not available</string>
<string name="system_install_denied_body">This option is only available when F-Droid Privileged Extension is installed.</string>
@ -339,10 +338,7 @@
<string name="tap_to_install_format">Tap to install %s</string>
<string name="tap_to_update_format">Tap to update %s</string>
<string name="install_confirm">Do you want to install this application?
It will get access to:</string>
<string name="install_confirm_no_perms">Do you want to install this application?
It does not require any special access.</string>
<string name="install_confirm">needs access to</string>
<string name="install_confirm_update">Do you want to install an update
to this existing application? Your existing data will not
be lost. The updated application will get access to:</string>

View File

@ -48,6 +48,20 @@
<item name="colorAccent">@color/fdroid_green</item>
</style>
<style name="MinWithDialogBaseThemeDark" parent="Theme.AppCompat.Dialog.MinWidth">
<item name="colorAccent">@color/fdroid_green</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="MinWithDialogBaseThemeLight" parent="Theme.AppCompat.Light.Dialog.MinWidth">
<item name="colorAccent">@color/fdroid_green</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="TextViewStyle" parent="android:Widget.TextView">
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>