diff --git a/res/layout/fdroid.xml b/res/layout/fdroid.xml index 508ec029b..5f84b230b 100644 --- a/res/layout/fdroid.xml +++ b/res/layout/fdroid.xml @@ -1,22 +1,15 @@ - + - - - - - - - + diff --git a/src/org/fdroid/fdroid/AppListManager.java b/src/org/fdroid/fdroid/AppListManager.java new file mode 100644 index 000000000..59f66f161 --- /dev/null +++ b/src/org/fdroid/fdroid/AppListManager.java @@ -0,0 +1,234 @@ +package org.fdroid.fdroid; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.ArrayAdapter; + +import java.util.*; + +/** + * Should be owned by the FDroid Activity, but used by the AppListFragments. + * The idea is that it takes a non-trivial amount of time to work this stuff + * out, and it is quicker if we only do it once for each view, rather than + * each fragment figuring out their own list independently. + */ +public class AppListManager { + + private Vector allApps = null; + + private FDroid fdroidActivity; + + private AppListAdapter availableApps; + private AppListAdapter installedApps; + private AppListAdapter canUpgradeApps; + private ArrayAdapter categories; + + private String currentCategory = null; + private String categoryAll = null; + private String categoryWhatsNew = null; + private String categoryRecentlyUpdated = null; + + public AppListAdapter getAvailableAdapter() { + return availableApps; + } + + public AppListAdapter getInstalledAdapter() { + return installedApps; + } + + public AppListAdapter getCanUpdateAdapter() { + return canUpgradeApps; + } + + public ArrayAdapter getCategoriesAdapter() { + return categories; + } + + public AppListManager(FDroid activity) { + this.fdroidActivity = activity; + + availableApps = new AppListAdapter(fdroidActivity); + installedApps = new AppListAdapter(fdroidActivity); + canUpgradeApps = new AppListAdapter(fdroidActivity); + + // Needs to be created before createViews(), because that will use the + // getCategoriesAdapter() accessor which expects this object... + categories = new ArrayAdapter(activity, + android.R.layout.simple_spinner_item, new Vector()); + categories + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + } + + private void clear() { + installedApps.clear(); + availableApps.clear(); + canUpgradeApps.clear(); + categories.clear(); + } + + private void notifyLists() { + // Tell the lists that the data behind the adapter has changed, so + // they can refresh... + availableApps.notifyDataSetChanged(); + installedApps.notifyDataSetChanged(); + canUpgradeApps.notifyDataSetChanged(); + categories.notifyDataSetChanged(); + } + + private void updateCategories() { + try { + DB db = DB.getDB(); + + // Populate the category list with the real categories, and the + // locally generated meta-categories for "All", "What's New" and + // "Recently Updated"... + categoryAll = fdroidActivity.getString(R.string.category_all); + categoryWhatsNew = fdroidActivity.getString(R.string.category_whatsnew); + categoryRecentlyUpdated = fdroidActivity.getString(R.string.category_recentlyupdated); + + categories.add(categoryWhatsNew); + categories.add(categoryRecentlyUpdated); + categories.add(categoryAll); + + for (String s : db.getCategories()) { + categories.add(s); + } + + if (currentCategory == null) + currentCategory = categoryWhatsNew; + + } finally { + DB.releaseDB(); + } + } + + // Tell the FDroid activity to update its "Update (x)" tab to correctly + // reflect the number of updates available. + private void notifyActivity() { + fdroidActivity.refreshUpdateTabLabel(); + } + + public void repopulateLists() { + + long startTime = System.currentTimeMillis(); + + clear(); + + updateCategories(); + updateApps(); + notifyLists(); + notifyActivity(); + + Log.d("FDroid", "Updated lists - " + allApps.size() + " allApps in total" + + " (update took " + (System.currentTimeMillis() - startTime) + + " ms)"); + } + + // Calculate the cutoff date we'll use for What's New and Recently + // Updated... + private Date calcMaxHistory() { + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(fdroidActivity.getBaseContext()); + String daysPreference = prefs.getString("updateHistoryDays", "14"); + int maxHistoryDays = Integer.parseInt(daysPreference); + Calendar recent = Calendar.getInstance(); + recent.add(Calendar.DAY_OF_YEAR, -maxHistoryDays); + return recent.getTime(); + } + + // recentDate could really be calculated here, but this is just a hack so + // it doesn't need to be caluculated for every single app. The reason it + // isn't an instance variable is because the preferences may change, and + // we wouldn't know. + private boolean isInCategory(DB.App app, String category, Date recentDate) { + boolean isInCategory; + if (category.equals(categoryAll)) { + isInCategory = true; + } else if (category.equals(categoryWhatsNew)) { + if (app.added == null) + isInCategory = false; + else if (app.added.compareTo(recentDate) < 0) + isInCategory = false; + else + isInCategory = true; + } else if (category.equals(categoryRecentlyUpdated)) { + if (app.lastUpdated == null) + isInCategory = false; + // Don't include in the recently updated category if the + // 'update' was actually it being added. + else if (app.lastUpdated.compareTo(app.added) == 0) + isInCategory = false; + else if (app.lastUpdated.compareTo(recentDate) < 0) + isInCategory = false; + else + isInCategory = true; + } else { + isInCategory = category.equals(app.category); + } + return isInCategory; + } + + // Returns false if the app list is empty and the fdroid activity decided + // to attempt updating it. + private boolean updateApps() { + + allApps = ((FDroidApp)fdroidActivity.getApplication()).getApps(); + + if (allApps.isEmpty()) { + // If its the first time we've run the app, this should update + // the repos. If not, it will do nothing, presuming that the repos + // are invalid, the internet is stuffed, the sky has fallen, etc... + return fdroidActivity.updateEmptyRepos(); + } + + Date recentDate = calcMaxHistory(); + AppFilter appFilter = new AppFilter(fdroidActivity); + + Vector availApps = new Vector(); + for (DB.App app : allApps) { + + boolean isInCategory = isInCategory(app, currentCategory, recentDate); + boolean isFiltered = appFilter.filter(app); + + // Add it to the list(s). Always to installed and updates, but + // only to available if it's not filtered. + if (!isFiltered && isInCategory) + availApps.add(app); + if (app.installedVersion != null) { + installedApps.addItem(app); + if (app.hasUpdates) + canUpgradeApps.addItem(app); + } + } + + if (currentCategory.equals(categoryWhatsNew)) { + Collections.sort(availApps, new WhatsNewComparator()); + } else if (currentCategory.equals(categoryRecentlyUpdated)) { + Collections.sort(availApps, new RecentlyUpdatedComparator()); + } + + for (DB.App app : availApps) + availableApps.addItem(app); + + return true; + } + + public void setCurrentCategory(String currentCategory) { + this.currentCategory = currentCategory; + } + + static class WhatsNewComparator implements Comparator { + @Override + public int compare(DB.App lhs, DB.App rhs) { + return rhs.added.compareTo(lhs.added); + } + } + + static class RecentlyUpdatedComparator implements Comparator { + @Override + public int compare(DB.App lhs, DB.App rhs) { + return rhs.lastUpdated.compareTo(lhs.lastUpdated); + } + } +} diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 712362dfc..4ee964b8b 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -19,6 +19,7 @@ package org.fdroid.fdroid; +import android.app.ActionBar; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; @@ -31,43 +32,34 @@ import org.fdroid.fdroid.R; import android.R.drawable; import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.app.TabActivity; import android.app.AlertDialog.Builder; +import android.app.FragmentTransaction; +import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.SharedPreferences; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; -import android.preference.PreferenceManager; +import android.support.v4.app.FragmentActivity; +import android.support.v4.view.ViewPager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup.LayoutParams; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.Spinner; -import android.widget.TabHost; -import android.widget.TextView; -import android.widget.Toast; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.*; import android.widget.TabHost.TabSpec; +import org.fdroid.fdroid.views.AppListFragmentPageAdapter; -public class FDroid extends TabActivity implements OnItemClickListener, - OnItemSelectedListener { +public class FDroid extends FragmentActivity { - private static final int REQUEST_APPDETAILS = 0; - private static final int REQUEST_MANAGEREPOS = 1; - private static final int REQUEST_PREFS = 2; + public static final int REQUEST_APPDETAILS = 0; + public static final int REQUEST_MANAGEREPOS = 1; + public static final int REQUEST_PREFS = 2; public static final String EXTRA_TAB_UPDATE = "extraTab"; @@ -77,70 +69,53 @@ public class FDroid extends TabActivity implements OnItemClickListener, private static final int ABOUT = Menu.FIRST + 3; private static final int SEARCH = Menu.FIRST + 4; - // Apps that are available to be installed - private AppListAdapter apps_av = new AppListAdapter(this); - - // Apps that are installed - private AppListAdapter apps_in = new AppListAdapter(this); - - // Apps that can be upgraded - private AppListAdapter apps_up = new AppListAdapter(this); - - // Category list - private ArrayAdapter categories; - private String currentCategory = null; - private ProgressDialog pd; - // Tags for the tabs - private static final String TAB_Installed = "I"; - private static final String TAB_Available = "A"; - private static final String TAB_Updates = "U"; + private ViewPager viewPager; + private AppListManager manager = null; + + // Used by pre 3.0 devices which don't have an ActionBar... private TabHost tabHost; - private TabSpec ts; - private TabSpec ts1; - private TabSpec tsUp; - private boolean triedEmptyUpdate; - - // List of apps. - private Vector apps = null; + public AppListManager getManager() { + return manager; + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + manager = new AppListManager(this); setContentView(R.layout.fdroid); - - categories = new ArrayAdapter(this, - android.R.layout.simple_spinner_item, new Vector()); - categories - .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - - tabHost = getTabHost(); + createViews(); createTabs(); + // Must be done *after* createViews, because it will involve a + // callback to update the tab label for the "update" tab. This + // will fail unless the tabs have actually been created. + repopulateViews(); + Intent i = getIntent(); if (i.hasExtra("uri")) { Intent call = new Intent(this, ManageRepo.class); call.putExtra("uri", i.getStringExtra("uri")); startActivityForResult(call, REQUEST_MANAGEREPOS); } else if (i.hasExtra(EXTRA_TAB_UPDATE)) { - boolean updateTab = i.getBooleanExtra(EXTRA_TAB_UPDATE, false); - if (updateTab) { - tabHost.setCurrentTab(2); + boolean showUpdateTab = i.getBooleanExtra(EXTRA_TAB_UPDATE, false); + if (showUpdateTab) { + selectTab(2); } } - - triedEmptyUpdate = false; } @Override protected void onStart() { super.onStart(); - populateLists(); + } + + protected void repopulateViews() { + manager.repopulateLists(); } @Override @@ -226,7 +201,6 @@ public class FDroid extends TabActivity implements OnItemClickListener, @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - triedEmptyUpdate = true; switch (requestCode) { case REQUEST_APPDETAILS: break; @@ -263,219 +237,142 @@ public class FDroid extends TabActivity implements OnItemClickListener, && (data.hasExtra("reset") || data.hasExtra("update"))) { updateRepos(); } else { - populateLists(); + repopulateViews(); } break; } } - private void createTabs() { - tabHost.clearAllTabs(); + private void createViews() { + viewPager = (ViewPager)findViewById(R.id.main_pager); + AppListFragmentPageAdapter viewPageAdapter = new AppListFragmentPageAdapter(this); + viewPager.setAdapter(viewPageAdapter); + viewPager.setOnPageChangeListener( new ViewPager.SimpleOnPageChangeListener() { + public void onPageSelected(int position) { + selectTab(position); + } + }); + } - // TabContentFactory that can generate the appropriate list for each - // tab... - TabHost.TabContentFactory tf = new TabHost.TabContentFactory() { + private void createTabs() { + if (Build.VERSION.SDK_INT >= 11) { + createActionBarTabs(); + } else { + createOldTabs(); + } + } + + private void selectTab(int index) { + if (Build.VERSION.SDK_INT >= 11) { + getActionBar().setSelectedNavigationItem(index); + } else { + tabHost.setCurrentTab(index); + } + } + + public void refreshUpdateTabLabel() { + final int INDEX = 2; + CharSequence text = viewPager.getAdapter().getPageTitle(INDEX); + if ( Build.VERSION.SDK_INT >= 11) { + getActionBar().getTabAt(INDEX).setText(text); + } else { + // Update the count on the 'Updates' tab to show the number available. + // This is quite unpleasant, but seems to be the only way to do it. + TextView textView = (TextView) tabHost.getTabWidget().getChildAt(2) + .findViewById(android.R.id.title); + textView.setText(text); + } + } + + private void createActionBarTabs() { + final ActionBar actionBar = getActionBar(); + final ViewPager pager = viewPager; + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + for (int i = 0; i < viewPager.getAdapter().getCount(); i ++) { + CharSequence label = viewPager.getAdapter().getPageTitle(i); + actionBar.addTab( + actionBar.newTab() + .setText(label) + .setTabListener(new ActionBar.TabListener() { + public void onTabSelected(ActionBar.Tab tab, + FragmentTransaction ft) { + pager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) { + } + + @Override + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) { + } + })); + } + } + + /** + * There is a bit of boiler-plate code required to get a TabWidget showing, + * which includes creating a TabHost, populating it with the TabWidget, + * and giving it a FrameLayout as a child. This will make the tabs have + * dummy empty contents and then hook them up to our ViewPager. + */ + private void createOldTabs() { + tabHost = new TabHost(this); + tabHost.setLayoutParams(new TabHost.LayoutParams( + TabHost.LayoutParams.MATCH_PARENT, TabHost.LayoutParams.WRAP_CONTENT)); + + TabWidget tabWidget = new TabWidget(this); + tabWidget.setId(android.R.id.tabs); + tabHost.setLayoutParams(new TabHost.LayoutParams( + TabWidget.LayoutParams.MATCH_PARENT, TabWidget.LayoutParams.WRAP_CONTENT)); + + FrameLayout layout = new FrameLayout(this); + layout.setId(android.R.id.tabcontent); + layout.setLayoutParams(new TabWidget.LayoutParams(0, 0)); + + tabHost.addView(tabWidget); + tabHost.addView(layout); + tabHost.setup(); + + TabHost.TabContentFactory factory = new TabHost.TabContentFactory() { @Override public View createTabContent(String tag) { - - AppListAdapter ad; - if (tag.equals(TAB_Installed)) - ad = apps_in; - else if (tag.equals(TAB_Updates)) - ad = apps_up; - else - ad = apps_av; - - ListView lst = new ListView(FDroid.this); - lst.setFastScrollEnabled(true); - lst.setOnItemClickListener(FDroid.this); - lst.setAdapter(ad); - - if (!tag.equals(TAB_Available)) - return lst; - - LinearLayout v = new LinearLayout(FDroid.this); - v.setOrientation(LinearLayout.VERTICAL); - Spinner cats = new Spinner(FDroid.this); - // Giving it an ID lets the default save/restore state - // functionality do its stuff. - cats.setId(R.id.categorySpinner); - cats.setAdapter(categories); - cats.setOnItemSelectedListener(FDroid.this); - v.addView(cats, new LayoutParams( - LinearLayout.LayoutParams.FILL_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); - v.addView(lst, new LayoutParams( - LinearLayout.LayoutParams.FILL_PARENT, - LinearLayout.LayoutParams.FILL_PARENT)); - return v; + return new View(FDroid.this); } }; - // Create the tab of installed apps... - ts = tabHost.newTabSpec(TAB_Installed); - ts.setIndicator(getString(R.string.tab_installed), getResources() - .getDrawable(drawable.star_off)); - ts.setContent(tf); + TabSpec availableTabSpec = tabHost.newTabSpec("available") + .setIndicator( + getString(R.string.tab_noninstalled), + getResources().getDrawable(android.R.drawable.ic_input_add)) + .setContent(factory); - // Create the tab of apps with updates... - tsUp = tabHost.newTabSpec(TAB_Updates); - tsUp.setIndicator(getString(R.string.tab_updates), getResources() - .getDrawable(drawable.star_on)); - tsUp.setContent(tf); + TabSpec installedTabSpec = tabHost.newTabSpec("installed") + .setIndicator( + getString(R.string.tab_installed), + getResources().getDrawable(android.R.drawable.star_off)) + .setContent(factory); - // Create the tab of available apps... - ts1 = tabHost.newTabSpec(TAB_Available); - ts1.setIndicator(getString(R.string.tab_noninstalled), getResources() - .getDrawable(drawable.ic_input_add)); - ts1.setContent(tf); + TabSpec canUpdateTabSpec = tabHost.newTabSpec("canUpdate") + .setIndicator( + getString(R.string.tab_updates), + getResources().getDrawable(android.R.drawable.star_on)) + .setContent(factory); - tabHost.addTab(ts1); - tabHost.addTab(ts); - tabHost.addTab(tsUp); + tabHost.addTab(availableTabSpec); + tabHost.addTab(installedTabSpec); + tabHost.addTab(canUpdateTabSpec); - } + LinearLayout contentView = (LinearLayout)findViewById(R.id.fdroid_layout); + contentView.addView(tabHost, 0); - // Populate the lists. - private void populateLists() { - - apps_in.clear(); - apps_av.clear(); - apps_up.clear(); - categories.clear(); - - long startTime = System.currentTimeMillis(); - - DB db; - String cat_all, cat_whatsnew, cat_recentlyupdated; - try { - db = DB.getDB(); - - // Populate the category list with the real categories, and the - // locally generated meta-categories for "All", "What's New" and - // "Recently Updated"... - cat_all = getString(R.string.category_all); - cat_whatsnew = getString(R.string.category_whatsnew); - cat_recentlyupdated = getString(R.string.category_recentlyupdated); - categories.add(cat_whatsnew); - categories.add(cat_recentlyupdated); - categories.add(cat_all); - for (String s : db.getCategories()) { - categories.add(s); + tabHost.setOnTabChangedListener( new TabHost.OnTabChangeListener() { + @Override + public void onTabChanged(String tabId) { + viewPager.setCurrentItem(tabHost.getCurrentTab()); } - if (currentCategory == null) - currentCategory = cat_whatsnew; - - } finally { - DB.releaseDB(); - } - - apps = ((FDroidApp) getApplication()).getApps(); - - if (apps.isEmpty()) { - // Don't attempt this more than once - we may have invalid - // repositories. - if (triedEmptyUpdate) - return; - // If there are no apps, update from the repos - it must be a - // new installation. - Log.d("FDroid", "Empty app list forces repo update"); - updateRepos(); - triedEmptyUpdate = true; - return; - } - - // Calculate the cutoff date we'll use for What's New and Recently - // Updated... - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(getBaseContext()); - String sint = prefs.getString("updateHistoryDays", "14"); - int history_days = Integer.parseInt(sint); - - Calendar recent = Calendar.getInstance(); - recent.add(Calendar.DAY_OF_YEAR, -history_days); - Date recentDate = recent.getTime(); - - AppFilter appfilter = new AppFilter(this); - - boolean incat; - Vector availapps = new Vector(); - for (DB.App app : apps) { - if (currentCategory.equals(cat_all)) { - incat = true; - } else if (currentCategory.equals(cat_whatsnew)) { - if (app.added == null) - incat = false; - else if (app.added.compareTo(recentDate) < 0) - incat = false; - else - incat = true; - } else if (currentCategory.equals(cat_recentlyupdated)) { - if (app.lastUpdated == null) - incat = false; - // Don't include in the recently updated category if the - // 'update' was actually it being added. - else if (app.lastUpdated.compareTo(app.added) == 0) - incat = false; - else if (app.lastUpdated.compareTo(recentDate) < 0) - incat = false; - else - incat = true; - } else { - incat = currentCategory.equals(app.category); - } - - boolean filtered = appfilter.filter(app); - - // Add it to the list(s). Always to installed and updates, but - // only to available if it's not filtered. - if (!filtered && incat) - availapps.add(app); - if (app.installedVersion != null) { - apps_in.addItem(app); - if (app.hasUpdates) - apps_up.addItem(app); - } - } - - if (currentCategory.equals(cat_whatsnew)) { - class WhatsNewComparator implements Comparator { - @Override - public int compare(App lhs, App rhs) { - return rhs.added.compareTo(lhs.added); - } - } - Collections.sort(availapps, new WhatsNewComparator()); - } else if (currentCategory.equals(cat_recentlyupdated)) { - class UpdatedComparator implements Comparator { - @Override - public int compare(App lhs, App rhs) { - return rhs.lastUpdated.compareTo(lhs.lastUpdated); - } - } - Collections.sort(availapps, new UpdatedComparator()); - } - for (DB.App app : availapps) - apps_av.addItem(app); - - // Update the count on the 'Updates' tab to show the number available. - // This is quite unpleasant, but seems to be the only way to do it. - TextView uptext = (TextView) tabHost.getTabWidget().getChildAt(2) - .findViewById(android.R.id.title); - uptext.setText(getString(R.string.tab_updates) + " (" - + Integer.toString(apps_up.getCount()) + ")"); - - // Tell the lists that the data behind the adapter has changed, so - // they can refresh... - apps_av.notifyDataSetChanged(); - apps_in.notifyDataSetChanged(); - apps_up.notifyDataSetChanged(); - categories.notifyDataSetChanged(); - - Log.d("FDroid", "Updated lists - " + apps.size() + " apps in total" - + " (update took " + (System.currentTimeMillis() - startTime) - + " ms)"); + }); } // For receiving results from the UpdateService when we've told it to @@ -491,7 +388,7 @@ public class FDroid extends TabActivity implements OnItemClickListener, Toast.makeText(FDroid.this, resultData.getString("errmsg"), Toast.LENGTH_LONG).show(); } else { - populateLists(); + repopulateViews(); } if (pd.isShowing()) pd.dismiss(); @@ -500,10 +397,32 @@ public class FDroid extends TabActivity implements OnItemClickListener, private UpdateReceiver mUpdateReceiver; + /** + * The first time the app is run, we will have an empty app list. + * If this is the case, we will attempt to update with the default repo. + * However, if we have tried this at least once, then don't try to do + * it automatically again, because the repos or internet connection may + * be bad. + */ + public boolean updateEmptyRepos() { + final String TRIED_EMPTY_UPDATE = "triedEmptyUpdate"; + boolean hasTriedEmptyUpdate = getPreferences(MODE_PRIVATE).getBoolean(TRIED_EMPTY_UPDATE, false); + if (!hasTriedEmptyUpdate) { + Log.d("FDroid", "Empty app list, and we haven't done an update yet. Forcing repo update."); + getPreferences(MODE_PRIVATE).edit().putBoolean(TRIED_EMPTY_UPDATE, true).apply(); + updateRepos(); + return true; + } else { + Log.d("FDroid", "Empty app list, but it looks like we've had an update previously. Will not force repo update."); + return false; + } + } + // Force a repo update now. A progress dialog is shown and the UpdateService // is told to do the update, which will result in the database changing. The // UpdateReceiver class should get told when this is finished. - private void updateRepos() { + public void updateRepos() { + pd = ProgressDialog.show(this, getString(R.string.process_wait_title), getString(R.string.process_update_msg), true, true); pd.setIcon(android.R.drawable.ic_dialog_info); @@ -515,36 +434,4 @@ public class FDroid extends TabActivity implements OnItemClickListener, startService(intent); } - public void onItemSelected(AdapterView parent, View view, int pos, - long id) { - currentCategory = parent.getItemAtPosition(pos).toString(); - populateLists(); - } - - public void onNothingSelected(AdapterView parent) { - // We always have at least "All" - } - - // Handler for a click on one of the items in an application list. Pops - // up a dialog that shows the details of the application and all its - // available versions, with buttons to allow installation etc. - public void onItemClick(AdapterView arg0, View arg1, final int arg2, - long arg3) { - - final DB.App app; - String curtab = tabHost.getCurrentTabTag(); - if (curtab.equalsIgnoreCase(TAB_Installed)) { - app = (DB.App) apps_in.getItem(arg2); - } else if (curtab.equalsIgnoreCase(TAB_Updates)) { - app = (DB.App) apps_up.getItem(arg2); - } else { - app = (DB.App) apps_av.getItem(arg2); - } - - Intent intent = new Intent(this, AppDetails.class); - intent.putExtra("appid", app.id); - startActivityForResult(intent, REQUEST_APPDETAILS); - - } - } diff --git a/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java b/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java new file mode 100644 index 000000000..e817ebd29 --- /dev/null +++ b/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java @@ -0,0 +1,64 @@ +package org.fdroid.fdroid.views; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import org.fdroid.fdroid.AppListManager; +import org.fdroid.fdroid.FDroid; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.views.fragments.AvailableAppsFragment; +import org.fdroid.fdroid.views.fragments.CanUpdateAppsFragment; +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 { + + private FDroid parent = null; + + public AppListFragmentPageAdapter(FDroid parent) { + super(parent.getSupportFragmentManager()); + this.parent = parent; + } + + @Override + public Fragment getItem(int i) { + Fragment fragment = null; + if ( i == 0 ) { + fragment = new AvailableAppsFragment(); + } else if ( i == 1 ) { + fragment = new InstalledAppsFragment(); + } else if ( i == 2 ) { + fragment = new CanUpdateAppsFragment(); + } + return fragment; + } + + @Override + public int getCount() { + return 3; + } + + public String getPageTitle(int i) { + switch(i) { + case 0: + return parent.getString(R.string.tab_noninstalled); + case 1: + return parent.getString(R.string.tab_installed); + case 2: + String updates = parent.getString(R.string.tab_updates); + updates += " (" + parent.getManager().getCanUpdateAdapter().getCount() + ")"; + return updates; + default: + return ""; + } + } + +} diff --git a/src/org/fdroid/fdroid/views/AppListView.java b/src/org/fdroid/fdroid/views/AppListView.java new file mode 100644 index 000000000..4fbe716ed --- /dev/null +++ b/src/org/fdroid/fdroid/views/AppListView.java @@ -0,0 +1,41 @@ +package org.fdroid.fdroid.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.ListView; + +/** + * There are three main app-lists in the UI: + * - Available + * - Installed + * - Apps which can be updated + * This class provides a View which knows about these app lists, but can have + * different contents (e.g. a drop down list of categories). It allows us to + * get a reference to the selected item in the FDroid Activity, without having + * to know which list we are actually looking at. + */ +public class AppListView extends LinearLayout { + + private ListView appList; + + public AppListView(Context context) { + super(context); + } + + public AppListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AppListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setAppList(ListView appList) { + this.appList = appList; + } + + public ListView getAppList() { + return appList; + } +} diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java new file mode 100644 index 000000000..6ad8f9588 --- /dev/null +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -0,0 +1,66 @@ +package org.fdroid.fdroid.views.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ListView; +import org.fdroid.fdroid.*; +import org.fdroid.fdroid.views.AppListView; + +abstract class AppListFragment extends Fragment implements AdapterView.OnItemClickListener { + + private AppListManager appListManager; + private FDroid parent; + + protected abstract AppListAdapter getAppListAdapter(); + + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + parent = (FDroid)activity; + } catch (ClassCastException e) { + // I know fragments are meant to be activity agnostic, but I can't + // think of a better way to share the one application list between + // all three app list fragments. + throw new RuntimeException( + "AppListFragment can only be attached to FDroid activity. " + + "Here it was attached to a " + activity.getClass() ); + } + } + + public AppListManager getAppListManager() { + return parent.getManager(); + } + + protected AppListView createPlainAppList() { + AppListView view = new AppListView(getActivity()); + ListView list = createAppListView(); + view.addView( + list, + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + view.setAppList(list); + return view; + } + + protected ListView createAppListView() { + ListView list = new ListView(getActivity()); + list.setFastScrollEnabled(true); + list.setOnItemClickListener(this); + list.setAdapter(getAppListAdapter()); + return list; + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final DB.App app = (DB.App)getAppListAdapter().getItem(position); + Intent intent = new Intent(getActivity(), AppDetails.class); + intent.putExtra("appid", app.id); + startActivityForResult(intent, FDroid.REQUEST_APPDETAILS); + } +} diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java new file mode 100644 index 000000000..517476e79 --- /dev/null +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -0,0 +1,67 @@ +package org.fdroid.fdroid.views.fragments; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; +import org.fdroid.fdroid.AppListAdapter; +import org.fdroid.fdroid.AppListManager; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.views.AppListView; + +public class AvailableAppsFragment extends AppListFragment implements AdapterView.OnItemSelectedListener { + + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + AppListView view = new AppListView(getActivity()); + view.setOrientation(LinearLayout.VERTICAL); + + Spinner spinner = new Spinner(getActivity()); + // Giving it an ID lets the default save/restore state + // functionality do its stuff. + spinner.setId(R.id.categorySpinner); + spinner.setAdapter(getAppListManager().getCategoriesAdapter()); + spinner.setOnItemSelectedListener(this); + + view.addView( + spinner, + new ViewGroup.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + ListView list = createAppListView(); + view.setAppList(list); + view.addView( + list, + new ViewGroup.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT)); + + return view; + } + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + } + + public void onItemSelected(AdapterView parent, View view, int pos, + long id) { + getAppListManager().setCurrentCategory(parent.getItemAtPosition(pos).toString()); + getAppListManager().repopulateLists(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + + @Override + protected AppListAdapter getAppListAdapter() { + return getAppListManager().getAvailableAdapter(); + } +} diff --git a/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java new file mode 100644 index 000000000..2b864aadd --- /dev/null +++ b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java @@ -0,0 +1,20 @@ +package org.fdroid.fdroid.views.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import org.fdroid.fdroid.AppListAdapter; + +public class CanUpdateAppsFragment extends AppListFragment { + + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return createPlainAppList(); + } + + @Override + protected AppListAdapter getAppListAdapter() { + return getAppListManager().getCanUpdateAdapter(); + } +} diff --git a/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java new file mode 100644 index 000000000..e6b48f35f --- /dev/null +++ b/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java @@ -0,0 +1,20 @@ +package org.fdroid.fdroid.views.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import org.fdroid.fdroid.AppListAdapter; + +public class InstalledAppsFragment extends AppListFragment { + + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return createPlainAppList(); + } + + @Override + protected AppListAdapter getAppListAdapter() { + return getAppListManager().getInstalledAdapter(); + } +}