Merge branch 'appcompat-porting-and-fixes' of https://gitlab.com/eighthave/fdroidclient

This merge request is a lot of porting code to use Android Support
appcompat-v7, now that it is in place. There are places where old custom
compat layers are replaced by appcompat, and other places like the local repo
stuff, where appcompat allows for full support on platforms older than 11.
This commit is contained in:
Daniel Martí 2014-07-18 13:26:48 +02:00
commit 8fc125a1c5
27 changed files with 1106 additions and 1007 deletions

View File

@ -107,9 +107,13 @@
android:name="android.app.default_searchable"
android:value=".SearchResults" />
</activity>
<!--
The title for ManageReposActivity is "F-Droid" here, but "Repositories"
when viewing the Activity itself in the app.
-->
<activity
android:name=".ManageRepo"
android:label="@string/menu_manage"
android:name=".views.ManageReposActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:parentActivityName=".FDroid" >
<meta-data
@ -152,6 +156,8 @@
the QR Code for sending the local repo to another device.
-->
<data android:path="/FDROID/REPO" />
<data android:path="/.*/FDROID/REPO" />
<data android:path="/.*/.*/FDROID/REPO" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -218,7 +224,7 @@
<activity
android:name=".views.RepoDetailsActivity"
android:label="@string/menu_manage"
android:parentActivityName=".ManageRepo"
android:parentActivityName=".views.ManageReposActivity"
android:windowSoftInputMode="stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2010 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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:paddingBottom="2dip"
android:paddingTop="2dip" >
<ImageView
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginTop="6dip"
tools:ignore="ContentDescription" />
<TwoLineListItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:mode="twoLine" >
<TextView
android:id="@+id/application_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginTop="6dip"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/package_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/application_label"
android:layout_below="@+id/application_label"
android:textAppearance="?android:attr/textAppearanceSmall" />
</TwoLineListItem>
</LinearLayout>

View File

@ -1,28 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:baselineAligned="false" >
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingBottom="2dip"
android:paddingTop="2dip" >
<!-- Actual icon size is dependent on preferences and set in
AppListAdapater.java:layoutIcon() -->
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginTop="6dip"
android:scaleType="fitCenter"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingStart="10dp"
android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
android:layout_marginRight="?attr/listPreferredItemPaddingRight"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_gravity="center_vertical"
android:baselineAligned="false" >
@ -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"

View File

@ -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" >
<!-- TODO remove NewApi ignore when appcompat-v7 is in place -->
android:paddingTop="2dip" >
<ImageView
android:layout_width="48dip"
android:layout_height="48dip"
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"
tools:ignore="ContentDescription" />
@ -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" />
<TextView
android:id="@+id/package_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/application_label"
android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_below="@+id/application_label"
android:textAppearance="?android:attr/textAppearanceSmall" />
</TwoLineListItem>

View File

@ -1,20 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/menu_setup_repo"
android:icon="@android:drawable/ic_input_add"
android:showAsAction="ifRoom|withText"
android:title="@string/setup_repo"/>
android:title="@string/setup_repo"
app:showAsAction="ifRoom|withText"/>
<item
android:id="@+id/menu_send_fdroid_via_wifi"
android:icon="@android:drawable/arrow_up_float"
android:showAsAction="never"
android:title="@string/send_fdroid_via_wifi"/>
android:title="@string/send_fdroid_via_wifi"
app:showAsAction="never"/>
<item
android:id="@+id/menu_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:showAsAction="never"
android:title="@string/menu_preferences"/>
android:title="@string/menu_preferences"
app:showAsAction="never"/>
</menu>
</menu>

40
res/menu/main.xml Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/action_search"
android:icon="@android:drawable/ic_menu_search"
android:title="@string/menu_search"
app:showAsAction="always"/>
<item
android:id="@+id/action_update_repo"
android:icon="@drawable/ic_menu_refresh"
android:title="@string/menu_update_repo"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_local_repo"
android:title="@string/local_repo"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_manage_repos"
android:icon="@android:drawable/ic_menu_agenda"
android:title="@string/menu_manage"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_bluetooth_apk"
android:icon="@android:drawable/stat_sys_data_bluetooth"
android:title="@string/menu_send_apk_bt"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/menu_preferences"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_about"
android:icon="@android:drawable/ic_menu_help"
android:title="@string/menu_about"
app:showAsAction="ifRoom"/>
</menu>

20
res/menu/manage_repos.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/action_update_repo"
android:icon="@drawable/ic_menu_refresh"
android:title="@string/menu_update_repo"
app:showAsAction="always|withText"/>
<item
android:id="@+id/action_add_repo"
android:icon="@android:drawable/ic_menu_add"
android:title="@string/menu_add_repo"
app:showAsAction="always|withText"/>
<item
android:id="@+id/action_find_local_repos"
android:title="@string/menu_scan_repo"
app:showAsAction="ifRoom|withText"/>
</menu>

View File

@ -1,14 +1,17 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/action_search"
android:icon="@android:drawable/ic_menu_search"
android:showAsAction="ifRoom"
android:title="@string/menu_search"/>
android:title="@string/menu_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_update_repo"
android:icon="@android:drawable/ic_input_add"
android:showAsAction="always|withText"
android:title="@string/update_repo"/>
android:title="@string/update_repo"
app:showAsAction="always"/>
</menu>

View File

@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/action_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="collapseActionView|always"
android:icon="@android:drawable/ic_menu_search"
android:title="@string/menu_search"/>
android:title="@string/menu_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="collapseActionView|always"/>
<item
android:id="@+id/action_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/menu_preferences"
app:showAsAction="never"/>
app:showAsAction="ifRoom"/>
</menu>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="applist_icon_normal_size">48dp</dimen>
<dimen name="applist_icon_compact_size">32dp</dimen>
</resources>

View File

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="padding_side">8dp</dimen>
<dimen name="padding_top">5dp</dimen>
<dimen name="form_label_top">5dp</dimen>
<dimen name="applist_icon_normal_size">48dp</dimen>
<dimen name="applist_icon_compact_size">32dp</dimen>
</resources>

View File

@ -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;
}

View File

@ -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) {

View File

@ -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!
}
}
}
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -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;

View File

@ -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 + ")";
}

View File

@ -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;
}

View File

@ -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<Cursor>, RepoAdapter.EnabledListener {
@Override
public Loader<Cursor> 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<Cursor> cursorLoader, Cursor cursor) {
repoAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> 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);
}
}
}

View File

@ -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;
}

View File

@ -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<Cursor>, 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<Cursor> 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<Cursor> cursorLoader, Cursor cursor) {
repoAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> 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;
}
}

View File

@ -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<Cursor> 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<Cursor> loader) {
((SimpleCursorAdapter) this.getListAdapter()).swapCursor(null);
SimpleCursorAdapter adapter = (SimpleCursorAdapter) getListAdapter();
adapter.swapCursor(null);
}
@Override

Binary file not shown.

View File

@ -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!
}
}
}