diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2e4a2c93e..00136bf77 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -107,9 +107,13 @@ android:name="android.app.default_searchable" android:value=".SearchResults" /> + + + @@ -218,7 +224,7 @@ + + + + + + + + + + + + + + diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index 793e46319..d37feaa7d 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -1,28 +1,32 @@ + android:minHeight="?android:attr/listPreferredItemHeight" + android:paddingBottom="2dip" + android:paddingTop="2dip" > @@ -54,8 +58,8 @@ android:layout_weight="1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="8dp" - android:layout_marginStart="8dp" + android:layout_marginLeft="?attr/listPreferredItemPaddingLeft" + android:layout_marginStart="?android:attr/listPreferredItemPaddingStart" android:layout_gravity="center_vertical" android:gravity="end" android:textAlignment="viewEnd" @@ -89,8 +93,8 @@ android:layout_weight="1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="6sp" - android:layout_marginStart="6sp" + android:layout_marginLeft="?attr/listPreferredItemPaddingLeft" + android:layout_marginStart="?android:attr/listPreferredItemPaddingStart" android:layout_gravity="center_vertical" android:gravity="end" android:textAlignment="viewEnd" diff --git a/res/layout/select_local_apps_list_item.xml b/res/layout/select_local_apps_list_item.xml index 895d3a3a1..36e5a7bf3 100644 --- a/res/layout/select_local_apps_list_item.xml +++ b/res/layout/select_local_apps_list_item.xml @@ -16,19 +16,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:attr/activatedBackgroundIndicator" - android:minHeight="?android:attr/listPreferredItemHeight" + android:minHeight="?attr/listPreferredItemHeight" android:paddingBottom="2dip" - android:paddingTop="2dip" - tools:ignore="NewApi" > - - + android:paddingTop="2dip" > @@ -41,16 +37,17 @@ android:id="@+id/application_label" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft" - android:layout_marginStart="?android:attr/listPreferredItemPaddingLeft" + android:layout_marginLeft="?attr/listPreferredItemPaddingLeft" + android:layout_marginStart="?android:attr/listPreferredItemPaddingStart" android:layout_marginTop="6dip" - android:textAppearance="?android:attr/textAppearanceListItem" /> + android:textAppearance="?android:attr/textAppearanceMedium" /> diff --git a/res/menu/local_repo_activity.xml b/res/menu/local_repo_activity.xml index b81b2a670..606090278 100644 --- a/res/menu/local_repo_activity.xml +++ b/res/menu/local_repo_activity.xml @@ -1,20 +1,21 @@ - + + android:title="@string/setup_repo" + app:showAsAction="ifRoom|withText"/> + android:title="@string/send_fdroid_via_wifi" + app:showAsAction="never"/> + android:title="@string/menu_preferences" + app:showAsAction="never"/> - + \ No newline at end of file diff --git a/res/menu/main.xml b/res/menu/main.xml new file mode 100644 index 000000000..38e72966e --- /dev/null +++ b/res/menu/main.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/manage_repos.xml b/res/menu/manage_repos.xml new file mode 100644 index 000000000..6fa0b699a --- /dev/null +++ b/res/menu/manage_repos.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/res/menu/select_local_apps_action_mode.xml b/res/menu/select_local_apps_action_mode.xml index 5c09264f4..f0c9bc6f1 100644 --- a/res/menu/select_local_apps_action_mode.xml +++ b/res/menu/select_local_apps_action_mode.xml @@ -1,14 +1,17 @@ - + + + android:title="@string/menu_search" + app:actionViewClass="android.support.v7.widget.SearchView" + app:showAsAction="ifRoom"/> + android:title="@string/update_repo" + app:showAsAction="always"/> \ No newline at end of file diff --git a/res/menu/select_local_apps_activity.xml b/res/menu/select_local_apps_activity.xml index 98a321135..172e907bf 100644 --- a/res/menu/select_local_apps_activity.xml +++ b/res/menu/select_local_apps_activity.xml @@ -1,16 +1,17 @@ + + xmlns:app="http://schemas.android.com/apk/res-auto" > + android:title="@string/menu_search" + app:actionViewClass="android.support.v7.widget.SearchView" + app:showAsAction="collapseActionView|always"/> + app:showAsAction="ifRoom"/> \ No newline at end of file diff --git a/res/values/dimen.xml b/res/values/dimen.xml deleted file mode 100644 index f20a66205..000000000 --- a/res/values/dimen.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 48dp - 32dp - diff --git a/res/values/dimens.xml b/res/values/dimens.xml index fd954b7fa..037b9a36f 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -1,6 +1,10 @@ + 8dp 5dp 5dp + 48dp + 32dp + \ No newline at end of file diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index a1cea8a64..8b331c65d 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -59,12 +59,12 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; + import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.ImageScaleType; + import org.fdroid.fdroid.Utils.CommaSeparatedList; -import org.fdroid.fdroid.compat.ActionBarCompat; -import org.fdroid.fdroid.compat.MenuManager; import org.fdroid.fdroid.compat.PackageManagerCompat; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; @@ -126,18 +126,18 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A TextView added; TextView nativecode; } - + // observer to update view when package has been installed/deleted AppObserver myAppObserver; class AppObserver extends ContentObserver { public AppObserver(Handler handler) { - super(handler); + super(handler); } @Override public void onChange(boolean selfChange) { this.onChange(selfChange, null); - } + } @Override public void onChange(boolean selfChange, Uri uri) { @@ -147,7 +147,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } refreshApkList(); - MenuManager.create(AppDetails.this).invalidateOptionsMenu(); + supportInvalidateOptionsMenu(); } } @@ -384,7 +384,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A mPm = getPackageManager(); installer = Installer.getActivityInstaller(this, mPm, myInstallerCallback); - + // Get the preferences we're going to use in this Activity... ConfigurationChangeHelper previousData = (ConfigurationChangeHelper)getLastCustomNonConfigurationInstance(); if (previousData != null) { @@ -409,10 +409,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A // fragments, which rely on data from the activity that is set earlier in this method. setContentView(R.layout.app_details); - // Actionbar cannot be accessed until after setContentView (on 3.0 and 3.1 devices) - // see: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html - // for reason why. - ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Check for the presence of a view which only exists in the landscape view. // This seems to be the preferred way to interrogate the view, rather than @@ -463,7 +460,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A protected void onResumeFragments() { super.onResumeFragments(); refreshApkList(); - MenuManager.create(this).invalidateOptionsMenu(); + supportInvalidateOptionsMenu(); } /** @@ -800,6 +797,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } // Install the version of this app denoted by 'app.curApk'. + @Override public void install(final Apk apk) { String [] projection = { RepoProvider.DataColumns.ADDRESS }; Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); @@ -883,7 +881,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A public void onSuccess(final int operation) { runOnUiThread(new Runnable() { @Override - public void run() { + public void run() { if (operation == Installer.InstallerCallback.OPERATION_INSTALL) { PackageManagerCompat.setInstaller(mPm, app.id); } @@ -907,9 +905,9 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A @Override public void run() { setProgressBarIndeterminateVisibility(false); - + Log.e(TAG, "Installer aborted with errorCode: " + errorCode); - + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this); alertBuilder.setTitle(R.string.installer_error_title); alertBuilder.setMessage(R.string.installer_error_title); @@ -1049,7 +1047,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A if (installer.handleOnActivityResult(requestCode, resultCode, data)) { return; } - + switch (requestCode) { case REQUEST_ENABLE_BLUETOOTH: fdroidApp.sendViaBluetooth(this, resultCode, app.id); @@ -1057,18 +1055,22 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } } + @Override public App getApp() { return app; } + @Override public ApkListAdapter getApks() { return adapter; } + @Override public Signature getInstalledSignature() { return mInstalledSignature; } + @Override public String getInstalledSignatureId() { return mInstalledSigID; } diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index e66fcc7c9..85d142f25 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -32,7 +32,6 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.support.v4.view.MenuItemCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBarActivity; import android.util.Log; @@ -42,10 +41,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; + import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.views.AppListFragmentPageAdapter; +import org.fdroid.fdroid.views.AppListFragmentPagerAdapter; import org.fdroid.fdroid.views.LocalRepoActivity; +import org.fdroid.fdroid.views.ManageReposActivity; public class FDroid extends ActionBarActivity { @@ -56,14 +57,6 @@ public class FDroid extends ActionBarActivity { public static final String EXTRA_TAB_UPDATE = "extraTab"; - private static final int UPDATE_REPO = Menu.FIRST; - private static final int MANAGE_REPO = Menu.FIRST + 1; - private static final int PREFERENCES = Menu.FIRST + 2; - private static final int ABOUT = Menu.FIRST + 3; - private static final int SEARCH = Menu.FIRST + 4; - private static final int BLUETOOTH_APK = Menu.FIRST + 5; - private static final int LOCAL_REPO = Menu.FIRST + 6; - private FDroidApp fdroidApp = null; private ViewPager viewPager; @@ -124,23 +117,13 @@ public class FDroid extends ActionBarActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { - - super.onCreateOptionsMenu(menu); - menu.add(Menu.NONE, UPDATE_REPO, 1, R.string.menu_update_repo).setIcon( - android.R.drawable.ic_menu_rotate); - menu.add(Menu.NONE, MANAGE_REPO, 2, R.string.menu_manage).setIcon( - android.R.drawable.ic_menu_agenda); - MenuItem search = menu.add(Menu.NONE, SEARCH, 3, R.string.menu_search).setIcon( - android.R.drawable.ic_menu_search); - if (fdroidApp.bluetoothAdapter != null) // ignore on devices without Bluetooth - menu.add(Menu.NONE, BLUETOOTH_APK, 3, R.string.menu_send_apk_bt); - menu.add(Menu.NONE, LOCAL_REPO, 4, R.string.local_repo); - menu.add(Menu.NONE, PREFERENCES, 4, R.string.menu_preferences).setIcon( - android.R.drawable.ic_menu_preferences); - menu.add(Menu.NONE, ABOUT, 5, R.string.menu_about).setIcon( - android.R.drawable.ic_menu_help); - MenuItemCompat.setShowAsAction(search, MenuItemCompat.SHOW_AS_ACTION_ALWAYS); - return true; + getMenuInflater().inflate(R.menu.main, menu); + if (fdroidApp.bluetoothAdapter == null) { + // ignore on devices without Bluetooth + MenuItem btItem = menu.findItem(R.id.action_bluetooth_apk); + btItem.setVisible(false); + } + return super.onCreateOptionsMenu(menu); } @Override @@ -148,91 +131,91 @@ public class FDroid extends ActionBarActivity { switch (item.getItemId()) { - case UPDATE_REPO: - updateRepos(); - return true; + case R.id.action_update_repo: + updateRepos(); + return true; - case MANAGE_REPO: - Intent i = new Intent(this, ManageRepo.class); - startActivityForResult(i, REQUEST_MANAGEREPOS); - return true; + case R.id.action_manage_repos: + Intent i = new Intent(this, ManageReposActivity.class); + startActivityForResult(i, REQUEST_MANAGEREPOS); + return true; - case PREFERENCES: - Intent prefs = new Intent(getBaseContext(), PreferencesActivity.class); - startActivityForResult(prefs, REQUEST_PREFS); - return true; + case R.id.action_settings: + Intent prefs = new Intent(getBaseContext(), PreferencesActivity.class); + startActivityForResult(prefs, REQUEST_PREFS); + return true; - case LOCAL_REPO: - startActivity(new Intent(this, LocalRepoActivity.class)); - return true; + case R.id.action_local_repo: + startActivity(new Intent(this, LocalRepoActivity.class)); + return true; - case SEARCH: - onSearchRequested(); - return true; + case R.id.action_search: + onSearchRequested(); + return true; - case BLUETOOTH_APK: - /* - * If Bluetooth has not been enabled/turned on, then - * enabling device discoverability will automatically enable Bluetooth - */ - Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); - discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 121); - startActivityForResult(discoverBt, REQUEST_ENABLE_BLUETOOTH); - // if this is successful, the Bluetooth transfer is started - return true; + case R.id.action_bluetooth_apk: + /* + * If Bluetooth has not been enabled/turned on, then enabling + * device discoverability will automatically enable Bluetooth + */ + Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 121); + startActivityForResult(discoverBt, REQUEST_ENABLE_BLUETOOTH); + // if this is successful, the Bluetooth transfer is started + return true; - case ABOUT: - View view = null; - if (Build.VERSION.SDK_INT >= 11) { - LayoutInflater li = LayoutInflater.from(this); - view = li.inflate(R.layout.about, null); - } else { - view = View.inflate( - new ContextThemeWrapper(this, R.style.AboutDialogLight), - R.layout.about, null); - } + case R.id.action_about: + View view = null; + if (Build.VERSION.SDK_INT >= 11) { + LayoutInflater li = LayoutInflater.from(this); + view = li.inflate(R.layout.about, null); + } else { + view = View.inflate( + new ContextThemeWrapper(this, R.style.AboutDialogLight), + R.layout.about, null); + } - // Fill in the version... - try { - PackageInfo pi = getPackageManager() - .getPackageInfo(getApplicationContext() - .getPackageName(), 0); - ((TextView) view.findViewById(R.id.version)) - .setText(pi.versionName); - } catch (Exception e) { - } + // Fill in the version... + try { + PackageInfo pi = getPackageManager() + .getPackageInfo(getApplicationContext() + .getPackageName(), 0); + ((TextView) view.findViewById(R.id.version)) + .setText(pi.versionName); + } catch (Exception e) { + } - Builder p = null; - if (Build.VERSION.SDK_INT >= 11) { - p = new AlertDialog.Builder(this).setView(view); - } else { - p = new AlertDialog.Builder( - new ContextThemeWrapper( - this, R.style.AboutDialogLight) - ).setView(view); - } - final AlertDialog alrt = p.create(); - alrt.setIcon(R.drawable.ic_launcher); - alrt.setTitle(getString(R.string.about_title)); - alrt.setButton(AlertDialog.BUTTON_NEUTRAL, - getString(R.string.about_website), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - Uri uri = Uri.parse("https://f-droid.org"); - startActivity(new Intent(Intent.ACTION_VIEW, uri)); - } - }); - alrt.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.ok), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - } - }); - alrt.show(); - return true; + Builder p = null; + if (Build.VERSION.SDK_INT >= 11) { + p = new AlertDialog.Builder(this).setView(view); + } else { + p = new AlertDialog.Builder( + new ContextThemeWrapper( + this, R.style.AboutDialogLight) + ).setView(view); + } + final AlertDialog alrt = p.create(); + alrt.setIcon(R.drawable.ic_launcher); + alrt.setTitle(getString(R.string.about_title)); + alrt.setButton(AlertDialog.BUTTON_NEUTRAL, + getString(R.string.about_website), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + Uri uri = Uri.parse("https://f-droid.org"); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + }); + alrt.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.ok), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + } + }); + alrt.show(); + return true; } return super.onOptionsItemSelected(item); } @@ -244,7 +227,7 @@ public class FDroid extends ActionBarActivity { case REQUEST_APPDETAILS: break; case REQUEST_MANAGEREPOS: - if (data != null && data.hasExtra(ManageRepo.REQUEST_UPDATE)) { + if (data != null && data.hasExtra(ManageReposActivity.REQUEST_UPDATE)) { AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); ask_alrt.setTitle(getString(R.string.repo_update_title)); ask_alrt.setIcon(android.R.drawable.ic_menu_rotate); @@ -293,8 +276,8 @@ public class FDroid extends ActionBarActivity { private void createViews() { viewPager = (ViewPager)findViewById(R.id.main_pager); - AppListFragmentPageAdapter viewPageAdapter = new AppListFragmentPageAdapter(this); - viewPager.setAdapter(viewPageAdapter); + AppListFragmentPagerAdapter viewPagerAdapter = new AppListFragmentPagerAdapter(this); + viewPager.setAdapter(viewPagerAdapter); viewPager.setOnPageChangeListener( new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java deleted file mode 100644 index f7a32a3c5..000000000 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com - * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt - * - * 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; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.NavUtils; -import android.support.v7.app.ActionBarActivity; -import android.text.TextUtils; -import android.util.Log; -import android.view.MenuItem; -import android.widget.LinearLayout; -import android.widget.Toast; -import org.fdroid.fdroid.views.fragments.RepoListFragment; - -import java.util.Locale; - -public class ManageRepo extends ActionBarActivity { - - /** - * If we have a new repo added, or the address of a repo has changed, then - * we when we're finished, we'll set this boolean to true in the intent - * that we finish with, to signify that we want the main list of apps - * updated. - */ - public static final String REQUEST_UPDATE = "update"; - - private RepoListFragment listFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - - ((FDroidApp) getApplication()).applyTheme(this); - super.onCreate(savedInstanceState); - - FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentById(android.R.id.content) == null) { - // Need to set a dummy view (which will get overridden by the fragment manager - // below) so that we can call setContentView(). This is a work around for - // a (bug?) thing in 3.0, 3.1 which requires setContentView to be invoked before - // the actionbar is played with: - // http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html - setContentView( new LinearLayout(this) ); - - listFragment = new RepoListFragment(); - fm.beginTransaction() - .add(android.R.id.content, listFragment) - .commit(); - } - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - protected void onResume() { - super.onResume(); - /* let's see if someone is trying to send us a new repo */ - addRepoFromIntent(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - addRepoFromIntent(intent); - } - - @Override - public void finish() { - Intent ret = new Intent(); - markChangedIfRequired(ret); - setResult(Activity.RESULT_OK, ret); - super.finish(); - } - - private boolean hasChanged() { - return listFragment != null && listFragment.hasChanged(); - } - - private void markChangedIfRequired(Intent intent) { - if (hasChanged()) { - Log.i("FDroid", "Repo details have changed, prompting for update."); - intent.putExtra(REQUEST_UPDATE, true); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - Intent destIntent = new Intent(this, FDroid.class); - markChangedIfRequired(destIntent); - setResult(RESULT_OK, destIntent); - NavUtils.navigateUpTo(this, destIntent); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void addRepoFromIntent(Intent intent) { - /* an URL from a click, NFC, QRCode scan, etc */ - Uri uri = intent.getData(); - if (uri != null) { - // scheme and host should only ever be pure ASCII aka Locale.ENGLISH - String scheme = intent.getScheme(); - String host = uri.getHost(); - if (scheme == null || host == null) { - String msg = String.format(getString(R.string.malformed_repo_uri), uri); - Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); - return; - } - if (scheme.equals("FDROIDREPO") || scheme.equals("FDROIDREPOS")) { - /* - * QRCodes are more efficient in all upper case, so QR URIs are - * encoded in all upper case, then forced to lower case. - * Checking if the special F-Droid scheme being all is upper - * case means it should be downcased. - */ - uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); - } else if (uri.getPath().startsWith("/FDROID/REPO")) { - /* - * some QR scanners chop off the fdroidrepo:// and just try - * http://, then the incoming URI does not get downcased - * properly, and the query string is stripped off. So just - * downcase the path, and carry on to get something working. - */ - uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); - } - // make scheme and host lowercase so they're readable in dialogs - scheme = scheme.toLowerCase(Locale.ENGLISH); - host = host.toLowerCase(Locale.ENGLISH); - String fingerprint = uri.getQueryParameter("fingerprint"); - Log.i("RepoListFragment", "onCreate " + fingerprint); - if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo") - || scheme.equals("https") || scheme.equals("http")) { - - /* sanitize and format for function and readability */ - String uriString = uri.toString() - .replaceAll("\\?.*$", "") // remove the whole query - .replaceAll("/*$", "") // remove all trailing slashes - .replace(uri.getHost(), host) // downcase host name - .replace(intent.getScheme(), scheme) // downcase scheme - .replace("fdroidrepo", "http"); // proper repo address - listFragment.importRepo(uriString, fingerprint); - - // if this is a local repo, check we're on the same wifi - String uriBssid = uri.getQueryParameter("bssid"); - if (!TextUtils.isEmpty(uriBssid)) { - if (uri.getPort() != 8888 - && !host.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) { - Log.i("ManageRepo", "URI is not local repo: " + uri); - return; - } - WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); - WifiInfo wifiInfo = wifiManager.getConnectionInfo(); - String bssid = wifiInfo.getBSSID().toLowerCase(Locale.ENGLISH); - uriBssid = Uri.decode(uriBssid).toLowerCase(Locale.ENGLISH); - if (!bssid.equals(uriBssid)) { - String msg = String.format(getString(R.string.not_on_same_wifi), - uri.getQueryParameter("ssid")); - Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); - } - // TODO we should help the user to the right thing here, - // instead of just showing a message! - } - } - } - } -} diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 21dec8aa2..0638806c9 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -18,25 +18,49 @@ package org.fdroid.fdroid; -import android.app.*; -import android.content.*; +import android.app.AlarmManager; +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.ProgressDialog; +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; -import android.os.*; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; -import org.fdroid.fdroid.data.*; + +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.updater.RepoUpdater; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class UpdateService extends IntentService implements ProgressListener { diff --git a/src/org/fdroid/fdroid/compat/ActionBarCompat.java b/src/org/fdroid/fdroid/compat/ActionBarCompat.java deleted file mode 100644 index 9f78c0822..000000000 --- a/src/org/fdroid/fdroid/compat/ActionBarCompat.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.fdroid.fdroid.compat; - -import android.annotation.TargetApi; -import android.app.ActionBar; -import android.app.Activity; - -public abstract class ActionBarCompat extends Compatibility { - - public static ActionBarCompat create(Activity activity) { - if (hasApi(11)) { - return new HoneycombActionBarCompatImpl(activity); - } else { - return new OldActionBarCompatImpl(activity); - } - } - - protected final Activity activity; - - public ActionBarCompat(Activity activity) { - this.activity = activity; - } - - /** - * Cannot be accessed until after setContentView (on 3.0 and 3.1 devices) has - * been called on the relevant activity. If you don't have a content view - * (e.g. when using fragment manager to add fragments to the activity) then you - * will still need to call setContentView(), with a "new LinearLayout()" or something - * useless like that. - * See: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html - * for details. - */ - public abstract void setDisplayHomeAsUpEnabled(boolean value); - -} - -class OldActionBarCompatImpl extends ActionBarCompat { - - public OldActionBarCompatImpl(Activity activity) { - super(activity); - } - - @Override - public void setDisplayHomeAsUpEnabled(boolean value) { - // Do nothing... - } - -} - -@TargetApi(11) -class HoneycombActionBarCompatImpl extends ActionBarCompat { - - private final ActionBar actionBar; - - public HoneycombActionBarCompatImpl(Activity activity) { - super(activity); - this.actionBar = activity.getActionBar(); - } - - @Override - public void setDisplayHomeAsUpEnabled(boolean value) { - actionBar.setDisplayHomeAsUpEnabled(value); - } - -} diff --git a/src/org/fdroid/fdroid/compat/MenuManager.java b/src/org/fdroid/fdroid/compat/MenuManager.java deleted file mode 100644 index 08e2341b2..000000000 --- a/src/org/fdroid/fdroid/compat/MenuManager.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.fdroid.fdroid.compat; - -import android.annotation.TargetApi; -import android.app.Activity; - -abstract public class MenuManager extends Compatibility { - - public static MenuManager create(Activity activity) { - if (hasApi(11)) { - return new HoneycombMenuManagerImpl(activity); - } else { - return new OldMenuManagerImpl(activity); - } - } - - protected final Activity activity; - - protected MenuManager(Activity activity) { - this.activity = activity; - } - - abstract public void invalidateOptionsMenu(); - -} - -class OldMenuManagerImpl extends MenuManager { - - protected OldMenuManagerImpl(Activity activity) { - super(activity); - } - - @Override - public void invalidateOptionsMenu() { - } - -} - -@TargetApi(11) -class HoneycombMenuManagerImpl extends MenuManager { - - protected HoneycombMenuManagerImpl(Activity activity) { - super(activity); - } - - @Override - public void invalidateOptionsMenu() { - activity.invalidateOptionsMenu(); - } -} diff --git a/src/org/fdroid/fdroid/data/NewRepoConfig.java b/src/org/fdroid/fdroid/data/NewRepoConfig.java new file mode 100644 index 000000000..080c97b4a --- /dev/null +++ b/src/org/fdroid/fdroid/data/NewRepoConfig.java @@ -0,0 +1,137 @@ + +package org.fdroid.fdroid.data; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import org.fdroid.fdroid.R; + +import java.util.Arrays; +import java.util.Locale; + +public class NewRepoConfig { + + private String errorMessage; + private boolean isValidRepo; + + private String uriString; + private Uri uri; + private String host; + private int port = -1; + private String scheme; + private String fingerprint; + private String bssid; + private String ssid; + + public NewRepoConfig(Context context, Intent intent) { + /* an URL from a click, NFC, QRCode scan, etc */ + uri = intent.getData(); + if (uri == null) { + isValidRepo = false; + return; + } + + // scheme and host should only ever be pure ASCII aka Locale.ENGLISH + scheme = intent.getScheme(); + host = uri.getHost(); + port = uri.getPort(); + if (TextUtils.isEmpty(scheme) || TextUtils.isEmpty(host)) { + errorMessage = String.format(context.getString(R.string.malformed_repo_uri), + uri); + isValidRepo = false; + return; + } + + if (Arrays.asList("FDROIDREPO", "FDROIDREPOS").contains(scheme)) { + /* + * QRCodes are more efficient in all upper case, so QR URIs are + * encoded in all upper case, then forced to lower case. Checking if + * the special F-Droid scheme being all is upper case means it + * should be downcased. + */ + uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); + } else if (uri.getPath().endsWith("/FDROID/REPO")) { + /* + * some QR scanners chop off the fdroidrepo:// and just try http://, + * then the incoming URI does not get downcased properly, and the + * query string is stripped off. So just downcase the path, and + * carry on to get something working. + */ + uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); + } + + // make scheme and host lowercase so they're readable in dialogs + scheme = scheme.toLowerCase(Locale.ENGLISH); + host = host.toLowerCase(Locale.ENGLISH); + fingerprint = uri.getQueryParameter("fingerprint"); + bssid = uri.getQueryParameter("bssid"); + ssid = uri.getQueryParameter("ssid"); + + Log.i("RepoListFragment", "onCreate " + fingerprint); + if (Arrays.asList("fdroidrepos", "fdroidrepo", "https", "http").contains(scheme)) { + uriString = sanitizeRepoUri(uri); + } + + this.isValidRepo = true; + } + + public String getBssid() { + return bssid; + } + + public String getSsid() { + return ssid; + } + + public int getPort() { + return port; + } + + public String getUriString() { + return uriString; + } + + public String getHost() { + return host; + } + + public String getScheme() { + return scheme; + } + + public String getFingerprint() { + return fingerprint; + } + + public boolean isValidRepo() { + return isValidRepo; + } + + /* + * The port starts out as 8888, but if there is a conflict, it will be + * incremented until there is a free port found. + */ + public boolean looksLikeLocalRepo() { + return (port >= 8888 && host.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")); + } + + public String getErrorMessage() { + return errorMessage; + } + + /** Sanitize and format an incoming repo URI for function and readability */ + public static String sanitizeRepoUri(Uri uri) { + String scheme = uri.getScheme(); + String host = uri.getHost(); + return uri.toString() + .replaceAll("\\?.*$", "") // remove the whole query + .replaceAll("/*$", "") // remove all trailing slashes + .replace(host, host.toLowerCase(Locale.ENGLISH)) + .replace(scheme, scheme.toLowerCase(Locale.ENGLISH)) + .replace("fdroidrepo", "http") // proper repo address + .replace("/FDROID/REPO", "/fdroid/repo"); // for QR FDroid path + } +} diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index 013f4aad4..b8e1934fb 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -1,7 +1,6 @@ package org.fdroid.fdroid.views; import android.content.Context; -import android.content.pm.PackageInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.support.v4.widget.CursorAdapter; @@ -11,10 +10,12 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; + import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.ImageScaleType; import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; + import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.App; diff --git a/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java b/src/org/fdroid/fdroid/views/AppListFragmentPagerAdapter.java similarity index 92% rename from src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java rename to src/org/fdroid/fdroid/views/AppListFragmentPagerAdapter.java index 4313dec77..e56881b9a 100644 --- a/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListFragmentPagerAdapter.java @@ -2,6 +2,7 @@ package org.fdroid.fdroid.views; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentPagerAdapter; + import org.fdroid.fdroid.FDroid; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.AppProvider; @@ -13,18 +14,18 @@ import org.fdroid.fdroid.views.fragments.InstalledAppsFragment; * Used by the FDroid activity in conjunction with its ViewPager to support * swiping of tabs for both old devices (< 3.0) and new devices. */ -public class AppListFragmentPageAdapter extends FragmentPagerAdapter { +public class AppListFragmentPagerAdapter extends FragmentPagerAdapter { private FDroid parent = null; - public AppListFragmentPageAdapter(FDroid parent) { + public AppListFragmentPagerAdapter(FDroid parent) { super(parent.getSupportFragmentManager()); this.parent = parent; } private String getUpdateTabTitle() { int updateCount = AppProvider.Helper.count(parent, AppProvider.getCanUpdateUri()); - + // TODO: Make RTL friendly, probably by having a different string for both tab_updates_none and tab_updates return parent.getString(R.string.tab_updates) + " (" + updateCount + ")"; } diff --git a/src/org/fdroid/fdroid/views/LocalRepoActivity.java b/src/org/fdroid/fdroid/views/LocalRepoActivity.java index 0741eb182..67ef20e07 100644 --- a/src/org/fdroid/fdroid/views/LocalRepoActivity.java +++ b/src/org/fdroid/fdroid/views/LocalRepoActivity.java @@ -30,6 +30,7 @@ import android.widget.Button; import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; + import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.PreferencesActivity; import org.fdroid.fdroid.QrGenAsyncTask; @@ -158,8 +159,6 @@ public class LocalRepoActivity extends ActionBarActivity { public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.local_repo_activity, menu); - if (Build.VERSION.SDK_INT < 14) // TODO remove after including appcompat-v7 - menu.findItem(R.id.menu_setup_repo).setVisible(false); return true; } diff --git a/src/org/fdroid/fdroid/views/ManageReposActivity.java b/src/org/fdroid/fdroid/views/ManageReposActivity.java new file mode 100644 index 000000000..7c22c7c5d --- /dev/null +++ b/src/org/fdroid/fdroid/views/ManageReposActivity.java @@ -0,0 +1,616 @@ +/* + * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * 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.views; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.NavUtils; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.ActionBarActivity; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import org.fdroid.fdroid.FDroid; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.ProgressListener; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; +import org.fdroid.fdroid.compat.ClipboardCompat; +import org.fdroid.fdroid.data.NewRepoConfig; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.net.MDnsHelper; +import org.fdroid.fdroid.net.MDnsHelper.DiscoveredRepo; +import org.fdroid.fdroid.net.MDnsHelper.RepoScanListAdapter; +import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; +import java.util.Locale; + +import javax.jmdns.ServiceInfo; + +public class ManageReposActivity extends ActionBarActivity { + + /** + * If we have a new repo added, or the address of a repo has changed, then + * we when we're finished, we'll set this boolean to true in the intent that + * we finish with, to signify that we want the main list of apps updated. + */ + public static final String REQUEST_UPDATE = "update"; + + private RepoListFragment listFragment; + private AlertDialog addRepoDialog; + private static final String DEFAULT_NEW_REPO_TEXT = "https://"; + + private enum PositiveAction { + ADD_NEW, ENABLE, IGNORE + } + + private PositiveAction positiveAction; + + private UpdateService.UpdateReceiver updateHandler = null; + + private static boolean changed = false; + + /** + * True if activity started with an intent such as from QR code. False if + * opened from, e.g. the main menu. + */ + private boolean isImportingRepo = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + ((FDroidApp) getApplication()).applyTheme(this); + super.onCreate(savedInstanceState); + + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentById(android.R.id.content) == null) { + /* + * Need to set a dummy view (which will get overridden by the + * fragment manager below) so that we can call setContentView(). + * This is a work around for a (bug?) thing in 3.0, 3.1 which + * requires setContentView to be invoked before the actionbar is + * played with: + * http://blog.perpetumdesign.com/2011/08/strange-case-of + * -dr-action-and-mr-bar.html + */ + setContentView(new LinearLayout(this)); + + listFragment = new RepoListFragment(); + fm.beginTransaction() + .add(android.R.id.content, listFragment) + .commit(); + } + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + // title is "Repositories" here, but "F-Droid" in VIEW Intent chooser + getSupportActionBar().setTitle(R.string.menu_manage); + } + + @Override + protected void onResume() { + super.onResume(); + if (updateHandler != null) { + updateHandler.showDialog(this); + } + /* let's see if someone is trying to send us a new repo */ + addRepoFromIntent(getIntent()); + } + + @Override + protected void onPause() { + super.onPause(); + if (updateHandler != null) { + updateHandler.hideDialog(); + } + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + } + + @Override + public void finish() { + Intent ret = new Intent(); + markChangedIfRequired(ret); + setResult(Activity.RESULT_OK, ret); + super.finish(); + } + + private boolean hasChanged() { + return changed; + } + + private void markChangedIfRequired(Intent intent) { + if (hasChanged()) { + Log.i("FDroid", "Repo details have changed, prompting for update."); + intent.putExtra(REQUEST_UPDATE, true); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manage_repos, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent destIntent = new Intent(this, FDroid.class); + markChangedIfRequired(destIntent); + setResult(RESULT_OK, destIntent); + NavUtils.navigateUpTo(this, destIntent); + return true; + case R.id.action_add_repo: + showAddRepo(); + return true; + case R.id.action_update_repo: + updateRepos(); + return true; + case R.id.action_find_local_repos: + scanForRepos(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void updateRepos() { + updateHandler = UpdateService.updateNow(this).setListener( + new ProgressListener() { + @Override + public void onProgress(Event event) { + if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) || + event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) { + // No need to prompt to update any more, we just + // did it! + changed = false; + } + + if (event.type.equals(UpdateService.EVENT_FINISHED)) { + updateHandler = null; + } + } + }); + } + + private void scanForRepos() { + final RepoScanListAdapter adapter = new RepoScanListAdapter(this); + final MDnsHelper mDnsHelper = new MDnsHelper(this, adapter); + + final View view = getLayoutInflater().inflate(R.layout.repodiscoverylist, null); + final ListView repoScanList = (ListView) view.findViewById(R.id.reposcanlist); + + final AlertDialog alrt = new AlertDialog.Builder(this).setView(view) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mDnsHelper.stopDiscovery(); + dialog.dismiss(); + } + }).create(); + + alrt.setTitle(R.string.local_repos_title); + alrt.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + mDnsHelper.stopDiscovery(); + } + }); + + repoScanList.setAdapter(adapter); + repoScanList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, final View view, + int position, long id) { + + final DiscoveredRepo discoveredService = + (DiscoveredRepo) parent.getItemAtPosition(position); + + final ServiceInfo serviceInfo = discoveredService.getServiceInfo(); + String type = serviceInfo.getPropertyString("type"); + String protocol = type.contains("fdroidrepos") ? "https:/" : "http:/"; + String path = serviceInfo.getPropertyString("path"); + if (TextUtils.isEmpty(path)) + path = "/fdroid/repo"; + String serviceUrl = protocol + serviceInfo.getInetAddresses()[0] + + ":" + serviceInfo.getPort() + path; + showAddRepo(serviceUrl, serviceInfo.getPropertyString("fingerprint")); + } + }); + + alrt.show(); + mDnsHelper.discoverServices(); + } + + public void importRepo(String uri, String fingerprint) { + isImportingRepo = true; + showAddRepo(uri, fingerprint); + } + + private void showAddRepo() { + /* + * If there is text in the clipboard, and it looks like a URL, use that. + * Otherwise use "https://" as default repo string. + */ + ClipboardCompat clipboard = ClipboardCompat.create(this); + String text = clipboard.getText(); + String fingerprint = null; + if (!TextUtils.isEmpty(text)) { + try { + new URL(text); + Uri uri = Uri.parse(text); + fingerprint = uri.getQueryParameter("fingerprint"); + // uri might contain a QR-style, all uppercase URL: + if (TextUtils.isEmpty(fingerprint)) + fingerprint = uri.getQueryParameter("FINGERPRINT"); + text = NewRepoConfig.sanitizeRepoUri(uri); + } catch (MalformedURLException e) { + text = null; + } + } + + if (TextUtils.isEmpty(text)) { + text = DEFAULT_NEW_REPO_TEXT; + } + showAddRepo(text, fingerprint); + } + + private void showAddRepo(String newAddress, String newFingerprint) { + final View view = getLayoutInflater().inflate(R.layout.addrepo, null); + addRepoDialog = new AlertDialog.Builder(this).setView(view).create(); + final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri); + final EditText fingerprintEditText = (EditText) view + .findViewById(R.id.edit_fingerprint); + + /* + * If the "add new repo" dialog is launched by an action outside of + * FDroid, i.e. a URL, then check to see if any existing repos match, + * and change the action accordingly. + */ + final Repo repo = (newAddress != null && isImportingRepo) + ? RepoProvider.Helper.findByAddress(this, newAddress) + : null; + + addRepoDialog.setIcon(android.R.drawable.ic_menu_add); + addRepoDialog.setTitle(getString(R.string.repo_add_title)); + addRepoDialog.setButton(DialogInterface.BUTTON_POSITIVE, + getString(R.string.repo_add_add), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + String fp = fingerprintEditText.getText().toString(); + + // the DB uses null for no fingerprint but the above + // code returns "" rather than null if its blank + if (fp.equals("")) + fp = null; + + if (positiveAction == PositiveAction.ADD_NEW) + createNewRepo(uriEditText.getText().toString(), fp); + else if (positiveAction == PositiveAction.ENABLE) + createNewRepo(repo); + } + }); + + addRepoDialog.setButton(DialogInterface.BUTTON_NEGATIVE, + getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + addRepoDialog.show(); + + final TextView overwriteMessage = (TextView) view.findViewById(R.id.overwrite_message); + overwriteMessage.setVisibility(View.GONE); + if (repo == null) { + // no existing repo, add based on what we have + positiveAction = PositiveAction.ADD_NEW; + } else { + // found the address in the DB of existing repos + final Button addButton = addRepoDialog.getButton(DialogInterface.BUTTON_POSITIVE); + addRepoDialog.setTitle(R.string.repo_exists); + overwriteMessage.setVisibility(View.VISIBLE); + if (newFingerprint != null) + newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH); + if (repo.fingerprint == null && newFingerprint != null) { + // we're upgrading from unsigned to signed repo + overwriteMessage.setText(R.string.repo_exists_add_fingerprint); + addButton.setText(R.string.add_key); + positiveAction = PositiveAction.ADD_NEW; + } else if (newFingerprint == null || newFingerprint.equals(repo.fingerprint)) { + // this entry already exists and is not enabled, offer to + // enable + // it + if (repo.inuse) { + addRepoDialog.dismiss(); + Toast.makeText(this, R.string.repo_exists_and_enabled, + Toast.LENGTH_LONG).show(); + return; + } else { + overwriteMessage.setText(R.string.repo_exists_enable); + addButton.setText(R.string.enable); + positiveAction = PositiveAction.ENABLE; + } + } else { + // same address with different fingerprint, this could be + // malicious, so force the user to manually delete the repo + // before adding this one + overwriteMessage.setTextColor(getResources().getColor(R.color.red)); + overwriteMessage.setText(R.string.repo_delete_to_overwrite); + addButton.setText(R.string.overwrite); + addButton.setEnabled(false); + positiveAction = PositiveAction.IGNORE; + } + } + + if (newFingerprint != null) + fingerprintEditText.setText(newFingerprint); + + if (newAddress != null) { + // This trick of emptying text then appending, + // rather than just setting in the first place, + // is neccesary to move the cursor to the end of the input. + uriEditText.setText(""); + uriEditText.append(newAddress); + } + } + + /** + * Adds a new repo to the database. + */ + private void createNewRepo(String address, String fingerprint) { + ContentValues values = new ContentValues(2); + values.put(RepoProvider.DataColumns.ADDRESS, address); + if (fingerprint != null && fingerprint.length() > 0) { + values.put(RepoProvider.DataColumns.FINGERPRINT, + fingerprint.toUpperCase(Locale.ENGLISH)); + } + RepoProvider.Helper.insert(this, values); + finishedAddingRepo(); + } + + /** + * Seeing as this repo already exists, we will force it to be enabled again. + */ + private void createNewRepo(Repo repo) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, 1); + RepoProvider.Helper.update(this, repo, values); + repo.inuse = true; + finishedAddingRepo(); + } + + /** + * If started by an intent that expects a result (e.g. QR codes) then we + * will set a result and finish. Otherwise, we'll refresh the list of repos + * to reflect the newly created repo. + */ + private void finishedAddingRepo() { + changed = true; + addRepoDialog = null; + if (isImportingRepo) { + setResult(Activity.RESULT_OK); + finish(); + } + } + + private void addRepoFromIntent(Intent intent) { + /* an URL from a click, NFC, QRCode scan, etc */ + NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent); + if (newRepoConfig.isValidRepo()) { + importRepo(newRepoConfig.getUriString(), newRepoConfig.getFingerprint()); + checkIfNewRepoOnSameWifi(newRepoConfig); + } else if (newRepoConfig.getErrorMessage() != null) { + Toast.makeText(this, newRepoConfig.getErrorMessage(), Toast.LENGTH_LONG).show(); + } + } + + private void checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) { + // if this is a local repo, check we're on the same wifi + if (!TextUtils.isEmpty(newRepo.getBssid())) { + WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + String bssid = wifiInfo.getBSSID().toLowerCase(Locale.ENGLISH); + String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH); + if (!bssid.equals(newRepoBssid)) { + String msg = String.format(getString(R.string.not_on_same_wifi), newRepo.getSsid()); + Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); + } + // TODO we should help the user to the right thing here, + // instead of just showing a message! + } + } + + public static class RepoListFragment extends ListFragment + implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri = RepoProvider.getContentUri(); + Log.i("FDroid", "Creating repo loader '" + uri + "'."); + String[] projection = new String[] { + RepoProvider.DataColumns._ID, + RepoProvider.DataColumns.NAME, + RepoProvider.DataColumns.PUBLIC_KEY, + RepoProvider.DataColumns.FINGERPRINT, + RepoProvider.DataColumns.IN_USE + }; + return new CursorLoader(getActivity(), uri, projection, null, null, null); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + repoAdapter.swapCursor(cursor); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + repoAdapter.swapCursor(null); + } + + /** + * NOTE: If somebody toggles a repo off then on again, it will have + * removed all apps from the index when it was toggled off, so when it + * is toggled on again, then it will require a refresh. Previously, I + * toyed with the idea of remembering whether they had toggled on or + * off, and then only actually performing the function when the activity + * stopped, but I think that will be problematic. What about when they + * press the home button, or edit a repos details? It will start to + * become somewhat-random as to when the actual enabling, disabling is + * performed. So now, it just does the disable as soon as the user + * clicks "Off" and then removes the apps. To compensate for the removal + * of apps from index, it notifies the user via a toast that the apps + * have been removed. Also, as before, it will still prompt the user to + * update the repos if you toggled on on. + */ + @Override + public void onSetEnabled(Repo repo, boolean isEnabled) { + if (repo.inuse != isEnabled) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0); + RepoProvider.Helper.update(getActivity(), repo, values); + + if (isEnabled) { + changed = true; + } else { + FDroidApp app = (FDroidApp) getActivity().getApplication(); + RepoProvider.Helper.purgeApps(getActivity(), repo, app); + String notification = getString(R.string.repo_disabled_notification, repo.name); + Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); + } + } + } + + private RepoAdapter repoAdapter; + + private View createHeaderView() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + TextView textLastUpdate = new TextView(getActivity()); + long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); + String lastUpdateCheck = ""; + if (lastUpdate == 0) { + lastUpdateCheck = getString(R.string.never); + } else { + Date d = new Date(lastUpdate); + lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) + + " " + DateFormat.getTimeFormat(getActivity()).format(d); + } + textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck)); + + int sidePadding = (int) getResources().getDimension(R.dimen.padding_side); + int topPadding = (int) getResources().getDimension(R.dimen.padding_top); + + textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding); + return textLastUpdate; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (getListAdapter() == null) { + /* + * Can't do this in the onCreate view, because "onCreateView" + * which returns the list view is "called between onCreate and + * onActivityCreated" according to the docs. + */ + getListView().addHeaderView(createHeaderView(), null, false); + + /* + * This could go in onCreate (and used to) but it needs to be + * called after addHeaderView, which can only be called after + * onCreate... + */ + repoAdapter = new RepoAdapter(getActivity(), null); + repoAdapter.setEnabledListener(this); + setListAdapter(repoAdapter); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setRetainInstance(true); + setHasOptionsMenu(true); + } + + @Override + public void onResume() { + super.onResume(); + + // Starts a new or restarts an existing Loader in this manager + getLoaderManager().restartLoader(0, null, this); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + + super.onListItemClick(l, v, position, id); + + Repo repo = new Repo((Cursor) getListView().getItemAtPosition(position)); + editRepo(repo); + } + + public static final int SHOW_REPO_DETAILS = 1; + + public void editRepo(Repo repo) { + Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); + intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId()); + startActivityForResult(intent, SHOW_REPO_DETAILS); + } + } +} diff --git a/src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java b/src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java index 8d009aeab..007193691 100644 --- a/src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java +++ b/src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java @@ -5,11 +5,12 @@ import android.content.Intent; import android.os.Bundle; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.ActionBarActivity; +import android.support.v7.view.ActionMode; import android.support.v7.widget.SearchView; -import android.view.ActionMode; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; + import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.PreferencesActivity; import org.fdroid.fdroid.R; @@ -51,10 +52,6 @@ public class SelectLocalAppsActivity extends ActionBarActivity { setResult(RESULT_CANCELED); finish(); return true; - case R.id.action_search: - SearchView searchView = (SearchView) item.getActionView(); - searchView.setIconified(false); - return true; case R.id.action_settings: startActivity(new Intent(this, PreferencesActivity.class)); return true; @@ -68,7 +65,6 @@ public class SelectLocalAppsActivity extends ActionBarActivity { public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.select_local_apps_action_mode, menu); - menu.findItem(R.id.action_search).setActionView(searchView); return true; } diff --git a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java deleted file mode 100644 index 4c5738e6c..000000000 --- a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java +++ /dev/null @@ -1,504 +0,0 @@ - -package org.fdroid.fdroid.views.fragments; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.ContentValues; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.v4.app.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v4.view.MenuItemCompat; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; -import org.fdroid.fdroid.FDroidApp; -import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.ProgressListener; -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.UpdateService; -import org.fdroid.fdroid.compat.ClipboardCompat; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.net.MDnsHelper; -import org.fdroid.fdroid.net.MDnsHelper.DiscoveredRepo; -import org.fdroid.fdroid.net.MDnsHelper.RepoScanListAdapter; -import org.fdroid.fdroid.views.RepoAdapter; -import org.fdroid.fdroid.views.RepoDetailsActivity; - -import javax.jmdns.ServiceInfo; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Date; -import java.util.Locale; - -public class RepoListFragment extends ListFragment - implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { - - private AlertDialog addRepoDialog; - private static final String DEFAULT_NEW_REPO_TEXT = "https://"; - private final int ADD_REPO = 1; - private final int UPDATE_REPOS = 2; - private final int SCAN_FOR_REPOS = 3; - - private UpdateService.UpdateReceiver updateHandler = null; - - public boolean hasChanged() { - return changed; - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - if (updateHandler != null) { - updateHandler.showDialog(getActivity()); - } - } - - @Override - public void onDetach() { - super.onDetach(); - if (updateHandler != null) { - updateHandler.hideDialog(); - } - } - - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - Uri uri = RepoProvider.getContentUri(); - Log.i("FDroid", "Creating repo loader '" + uri + "'."); - String[] projection = new String[] { - RepoProvider.DataColumns._ID, - RepoProvider.DataColumns.NAME, - RepoProvider.DataColumns.PUBLIC_KEY, - RepoProvider.DataColumns.FINGERPRINT, - RepoProvider.DataColumns.IN_USE - }; - return new CursorLoader(getActivity(), uri, projection, null, null, null); - } - - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - repoAdapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(Loader cursorLoader) { - repoAdapter.swapCursor(null); - } - - /** - * NOTE: If somebody toggles a repo off then on again, it will have removed - * all apps from the index when it was toggled off, so when it is toggled on - * again, then it will require a refresh. Previously, I toyed with the idea - * of remembering whether they had toggled on or off, and then only actually - * performing the function when the activity stopped, but I think that will - * be problematic. What about when they press the home button, or edit a - * repos details? It will start to become somewhat-random as to when the - * actual enabling, disabling is performed. So now, it just does the disable - * as soon as the user clicks "Off" and then removes the apps. To compensate - * for the removal of apps from index, it notifies the user via a toast that - * the apps have been removed. Also, as before, it will still prompt the - * user to update the repos if you toggled on on. - */ - @Override - public void onSetEnabled(Repo repo, boolean isEnabled) { - if (repo.inuse != isEnabled) { - ContentValues values = new ContentValues(1); - values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0); - RepoProvider.Helper.update(getActivity(), repo, values); - - if (isEnabled) { - changed = true; - } else { - FDroidApp app = (FDroidApp) getActivity().getApplication(); - RepoProvider.Helper.purgeApps(getActivity(), repo, app); - String notification = getString(R.string.repo_disabled_notification, repo.name); - Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); - } - } - } - - private enum PositiveAction { - ADD_NEW, ENABLE, IGNORE - } - - private PositiveAction positiveAction; - - private boolean changed = false; - - private RepoAdapter repoAdapter; - - /** - * True if activity started with an intent such as from QR code. False if - * opened from, e.g. the main menu. - */ - private boolean isImportingRepo = false; - - private View createHeaderView() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - TextView textLastUpdate = new TextView(getActivity()); - long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); - String lastUpdateCheck = ""; - if (lastUpdate == 0) { - lastUpdateCheck = getString(R.string.never); - } else { - Date d = new Date(lastUpdate); - lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) + - " " + DateFormat.getTimeFormat(getActivity()).format(d); - } - textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck)); - - int sidePadding = (int) getResources().getDimension(R.dimen.padding_side); - int topPadding = (int) getResources().getDimension(R.dimen.padding_top); - - textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding); - return textLastUpdate; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (getListAdapter() == null) { - // Can't do this in the onCreate view, because "onCreateView" which - // returns the list view is "called between onCreate and - // onActivityCreated" according to the docs. - getListView().addHeaderView(createHeaderView(), null, false); - - // This could go in onCreate (and used to) but it needs to be called - // after addHeaderView, which can only be called after onCreate... - repoAdapter = new RepoAdapter(getActivity(), null); - repoAdapter.setEnabledListener(this); - setListAdapter(repoAdapter); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - setRetainInstance(true); - setHasOptionsMenu(true); - } - - @Override - public void onResume() { - super.onResume(); - - // Starts a new or restarts an existing Loader in this manager - getLoaderManager().restartLoader(0, null, this); - } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - - super.onListItemClick(l, v, position, id); - - Repo repo = new Repo((Cursor) getListView().getItemAtPosition(position)); - editRepo(repo); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - - MenuItem updateItem = menu.add(Menu.NONE, UPDATE_REPOS, 1, - R.string.menu_update_repo).setIcon(R.drawable.ic_menu_refresh); - MenuItemCompat.setShowAsAction(updateItem, - MenuItemCompat.SHOW_AS_ACTION_ALWAYS | - MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - - MenuItem addItem = menu.add(Menu.NONE, ADD_REPO, 1, R.string.menu_add_repo).setIcon( - android.R.drawable.ic_menu_add); - MenuItemCompat.setShowAsAction(addItem, - MenuItemCompat.SHOW_AS_ACTION_ALWAYS | - MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - - if (Build.VERSION.SDK_INT >= 16) { - menu.add(Menu.NONE, SCAN_FOR_REPOS, 1, R.string.menu_scan_repo).setIcon( - android.R.drawable.ic_menu_search); - } - } - - public static final int SHOW_REPO_DETAILS = 1; - - public void editRepo(Repo repo) { - Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); - intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId()); - startActivityForResult(intent, SHOW_REPO_DETAILS); - } - - private void updateRepos() { - updateHandler = UpdateService.updateNow(getActivity()).setListener(new ProgressListener() { - @Override - public void onProgress(Event event) { - if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) || - event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) { - // No need to prompt to update any more, we just did it! - changed = false; - } - - if (event.type.equals(UpdateService.EVENT_FINISHED)) { - updateHandler = null; - } - } - }); - } - - private void scanForRepos() { - final Activity activity = getActivity(); - - final RepoScanListAdapter adapter = new RepoScanListAdapter(activity); - final MDnsHelper mDnsHelper = new MDnsHelper(activity, adapter); - - final View view = getLayoutInflater(null).inflate(R.layout.repodiscoverylist, null); - final ListView repoScanList = (ListView) view.findViewById(R.id.reposcanlist); - - final AlertDialog alrt = new AlertDialog.Builder(getActivity()).setView(view) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mDnsHelper.stopDiscovery(); - dialog.dismiss(); - } - }).create(); - - alrt.setTitle(R.string.local_repos_title); - alrt.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - mDnsHelper.stopDiscovery(); - } - }); - - repoScanList.setAdapter(adapter); - repoScanList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, final View view, - int position, long id) { - - final DiscoveredRepo discoveredService = - (DiscoveredRepo) parent.getItemAtPosition(position); - - final ServiceInfo serviceInfo = discoveredService.getServiceInfo(); - String type = serviceInfo.getPropertyString("type"); - String protocol = type.contains("fdroidrepos") ? "https:/" : "http:/"; - String path = serviceInfo.getPropertyString("path"); - if (TextUtils.isEmpty(path)) - path = "/fdroid/repo"; - String serviceUrl = protocol + serviceInfo.getInetAddresses()[0] - + ":" + serviceInfo.getPort() + path; - showAddRepo(serviceUrl, serviceInfo.getPropertyString("fingerprint")); - } - }); - - alrt.show(); - mDnsHelper.discoverServices(); - } - - public void importRepo(String uri, String fingerprint) { - isImportingRepo = true; - showAddRepo(uri, fingerprint); - } - - private void showAddRepo() { - showAddRepo(getNewRepoUri(), null); - } - - private void showAddRepo(String newAddress, String newFingerprint) { - View view = getLayoutInflater(null).inflate(R.layout.addrepo, null); - addRepoDialog = new AlertDialog.Builder(getActivity()).setView(view).create(); - final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri); - final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint); - - /* - * If the "add new repo" dialog is launched by an action outside of - * FDroid, i.e. a URL, then check to see if any existing repos match, - * and change the action accordingly. - */ - final Repo repo = (newAddress != null && isImportingRepo) - ? RepoProvider.Helper.findByAddress(getActivity(), newAddress) - : null; - - addRepoDialog.setIcon(android.R.drawable.ic_menu_add); - addRepoDialog.setTitle(getString(R.string.repo_add_title)); - addRepoDialog.setButton(DialogInterface.BUTTON_POSITIVE, - getString(R.string.repo_add_add), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - String fp = fingerprintEditText.getText().toString(); - - // the DB uses null for no fingerprint but the above - // code returns "" rather than null if its blank - if (fp.equals("")) - fp = null; - - if (positiveAction == PositiveAction.ADD_NEW) - createNewRepo(uriEditText.getText().toString(), fp); - else if (positiveAction == PositiveAction.ENABLE) - createNewRepo(repo); - } - }); - - addRepoDialog.setButton(DialogInterface.BUTTON_NEGATIVE, - getString(R.string.cancel), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - addRepoDialog.show(); - - final TextView overwriteMessage = (TextView) view.findViewById(R.id.overwrite_message); - overwriteMessage.setVisibility(View.GONE); - if (repo == null) { - // no existing repo, add based on what we have - positiveAction = PositiveAction.ADD_NEW; - } else { - // found the address in the DB of existing repos - final Button addButton = addRepoDialog.getButton(DialogInterface.BUTTON_POSITIVE); - addRepoDialog.setTitle(R.string.repo_exists); - overwriteMessage.setVisibility(View.VISIBLE); - if (newFingerprint != null) - newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH); - if (repo.fingerprint == null && newFingerprint != null) { - // we're upgrading from unsigned to signed repo - overwriteMessage.setText(R.string.repo_exists_add_fingerprint); - addButton.setText(R.string.add_key); - positiveAction = PositiveAction.ADD_NEW; - } else if (newFingerprint == null || newFingerprint.equals(repo.fingerprint)) { - // this entry already exists and is not enabled, offer to enable - // it - if (repo.inuse) { - addRepoDialog.dismiss(); - Toast.makeText(getActivity(), R.string.repo_exists_and_enabled, - Toast.LENGTH_LONG).show(); - return; - } else { - overwriteMessage.setText(R.string.repo_exists_enable); - addButton.setText(R.string.enable); - positiveAction = PositiveAction.ENABLE; - } - } else { - // same address with different fingerprint, this could be - // malicious, so force the user to manually delete the repo - // before adding this one - overwriteMessage.setTextColor(getResources().getColor(R.color.red)); - overwriteMessage.setText(R.string.repo_delete_to_overwrite); - addButton.setText(R.string.overwrite); - addButton.setEnabled(false); - positiveAction = PositiveAction.IGNORE; - } - } - - if (newFingerprint != null) - fingerprintEditText.setText(newFingerprint); - - if (newAddress != null) { - // This trick of emptying text then appending, - // rather than just setting in the first place, - // is neccesary to move the cursor to the end of the input. - uriEditText.setText(""); - uriEditText.append(newAddress); - } - } - - /** - * Adds a new repo to the database. - */ - private void createNewRepo(String address, String fingerprint) { - ContentValues values = new ContentValues(2); - values.put(RepoProvider.DataColumns.ADDRESS, address); - if (fingerprint != null && fingerprint.length() > 0) { - values.put(RepoProvider.DataColumns.FINGERPRINT, - fingerprint.toUpperCase(Locale.ENGLISH)); - } - RepoProvider.Helper.insert(getActivity(), values); - finishedAddingRepo(); - } - - /** - * Seeing as this repo already exists, we will force it to be enabled again. - */ - private void createNewRepo(Repo repo) { - ContentValues values = new ContentValues(1); - values.put(RepoProvider.DataColumns.IN_USE, 1); - RepoProvider.Helper.update(getActivity(), repo, values); - repo.inuse = true; - finishedAddingRepo(); - } - - /** - * If started by an intent that expects a result (e.g. QR codes) then we - * will set a result and finish. Otherwise, we'll refresh the list of repos - * to reflect the newly created repo. - */ - private void finishedAddingRepo() { - changed = true; - addRepoDialog = null; - if (isImportingRepo) { - getActivity().setResult(Activity.RESULT_OK); - getActivity().finish(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - if (item.getItemId() == ADD_REPO) { - showAddRepo(); - return true; - } else if (item.getItemId() == UPDATE_REPOS) { - updateRepos(); - return true; - } else if (item.getItemId() == SCAN_FOR_REPOS) { - scanForRepos(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - /** - * If there is text in the clipboard, and it looks like a URL, use that. - * Otherwise return "https://". - */ - private String getNewRepoUri() { - ClipboardCompat clipboard = ClipboardCompat.create(getActivity()); - String text = clipboard.getText(); - if (text != null) { - try { - new URL(text); - } catch (MalformedURLException e) { - text = null; - } - } - - if (text == null) { - text = DEFAULT_NEW_REPO_TEXT; - } - return text; - } -} diff --git a/src/org/fdroid/fdroid/views/fragments/SelectLocalAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/SelectLocalAppsFragment.java index 86abed345..01d037a55 100644 --- a/src/org/fdroid/fdroid/views/fragments/SelectLocalAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/SelectLocalAppsFragment.java @@ -23,16 +23,17 @@ import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; +import android.support.v4.widget.SimpleCursorAdapter; +import android.support.v4.widget.SimpleCursorAdapter.ViewBinder; +import android.support.v7.view.ActionMode; import android.support.v7.widget.SearchView.OnQueryTextListener; import android.text.TextUtils; -import android.view.ActionMode; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; -import android.widget.SimpleCursorAdapter; -import android.widget.SimpleCursorAdapter.ViewBinder; import android.widget.TextView; + import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.InstalledAppProvider; @@ -125,8 +126,8 @@ public class SelectLocalAppsFragment extends ListFragment public void onListItemClick(ListView l, View v, int position, long id) { if (mActionMode == null) mActionMode = selectLocalAppsActivity - .startActionMode(selectLocalAppsActivity.mActionModeCallback); - Cursor c = (Cursor) l.getAdapter().getItem(position); + .startSupportActionMode(selectLocalAppsActivity.mActionModeCallback); + Cursor c = (Cursor) getListAdapter().getItem(position); String packageName = c.getString(c.getColumnIndex(DataColumns.APP_ID)); if (FDroidApp.selectedApps.contains(packageName)) { FDroidApp.selectedApps.remove(packageName); @@ -155,7 +156,8 @@ public class SelectLocalAppsFragment extends ListFragment @Override public void onLoadFinished(Loader loader, Cursor cursor) { - ((SimpleCursorAdapter) this.getListAdapter()).swapCursor(cursor); + SimpleCursorAdapter adapter = (SimpleCursorAdapter) getListAdapter(); + adapter.swapCursor(cursor); ListView listView = getListView(); int count = listView.getCount(); @@ -183,7 +185,8 @@ public class SelectLocalAppsFragment extends ListFragment @Override public void onLoaderReset(Loader loader) { - ((SimpleCursorAdapter) this.getListAdapter()).swapCursor(null); + SimpleCursorAdapter adapter = (SimpleCursorAdapter) getListAdapter(); + adapter.swapCursor(null); } @Override diff --git a/test/assets/masterKeyIndex.jar b/test/assets/masterKeyIndex.jar new file mode 100644 index 000000000..5f33a8dd7 Binary files /dev/null and b/test/assets/masterKeyIndex.jar differ diff --git a/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java b/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java index 42d9ad28c..dc98ab5e8 100644 --- a/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java +++ b/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java @@ -12,7 +12,11 @@ import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.updater.RepoUpdater.UpdateException; -import java.io.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; @TargetApi(8) public class SignedRepoUpdaterTest extends InstrumentationTestCase { @@ -165,4 +169,18 @@ public class SignedRepoUpdaterTest extends InstrumentationTestCase { // success! } } + + public void testExtractIndexFromMasterKeyIndexJar() { + if (!testFilesDir.canWrite()) + return; + // this is supposed to fail + try { + repoUpdater.getIndexFromFile(getTestFile("masterKeyIndex.jar")); + fail(); + } catch (UpdateException e) { + // success! + } catch (SecurityException e) { + // success! + } + } }