WIP: Refactored select apps to swap into view.

Not worrying about styling yet, just functionality. Added an InnerView
interface that these views can implement. Currently it asks them to
populate the menu. It may be slightly inefficient if we end up with a
popup menu, because it is called onPrepareOptionsMenu, but expects the
inner view to inflate the menu. However, for swap this shouldn't be an
issue, as all the menus pretty much fit in the action bar of most screen
sizes.
This commit is contained in:
Peter Serwylo 2015-05-23 00:08:24 +10:00
parent b6415dadb4
commit 4f7f7b2cb5
8 changed files with 522 additions and 10 deletions

View File

@ -396,6 +396,17 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<activity
android:label="@string/menu_swap"
android:name=".views.swap.SwapWorkflowActivity"
android:parentActivityName=".FDroid"
android:theme="@style/SwapTheme.Wizard"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<activity
android:label="@string/swap"
android:name=".views.swap.SwapAppListActivity"

View File

@ -19,7 +19,7 @@
android:background="?android:attr/activatedBackgroundIndicator"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingBottom="2dip"
android:paddingTop="2dip" >
android:paddingTop="2dip">
<ImageView
android:id="@android:id/icon"

View File

@ -18,7 +18,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeight"
android:paddingBottom="2dip"
android:paddingTop="2dip" >
android:paddingTop="2dip">
<CheckBox
android:id="@+id/checkbox"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<org.fdroid.fdroid.views.swap.views.SelectAppsView
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</org.fdroid.fdroid.views.swap.views.SelectAppsView>

View File

@ -51,6 +51,7 @@ import org.fdroid.fdroid.views.AppListFragmentPagerAdapter;
import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.swap.ConnectSwapActivity;
import org.fdroid.fdroid.views.swap.SwapActivity;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
public class FDroid extends ActionBarActivity {
@ -257,7 +258,7 @@ public class FDroid extends ActionBarActivity {
return true;
case R.id.action_swap:
startActivity(new Intent(this, SwapActivity.class));
startActivity(new Intent(this, SwapWorkflowActivity.class));
return true;
case R.id.action_search:

View File

@ -0,0 +1,185 @@
package org.fdroid.fdroid.views.swap;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.LocalRepoManager;
import java.util.Set;
public class SwapWorkflowActivity extends FragmentActivity {
private ViewGroup container;
private enum State {
INTRO, SELECT_APPS, JOIN_WIFI
}
public interface InnerView {
/** @return True if the menu should be shown. */
boolean buildMenu(Menu menu, @NonNull MenuInflater inflater);
}
private State currentState = State.INTRO;
private InnerView currentView;
private boolean hasPreparedLocalRepo = false;
private UpdateAsyncTask updateSwappableAppsTask = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.swap_activity);
container = (ViewGroup) findViewById(R.id.fragment_container);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean parent = super.onPrepareOptionsMenu(menu);
boolean inner = currentView.buildMenu(menu, getMenuInflater());
return parent || inner;
}
@Override
protected void onResume() {
super.onResume();
showView();
}
private void showView() {
if (currentState == State.INTRO) {
showIntro();
}
}
private void inflateInnerView(@LayoutRes int viewRes) {
container.removeAllViews();
View view = ((LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(viewRes, container, false);
currentView = (InnerView)view;
container.addView(view);
supportInvalidateOptionsMenu();
}
private void showIntro() {
inflateInnerView(R.layout.swap_blank);
}
public void showSelectApps() {
inflateInnerView(R.layout.swap_select_apps);
}
// TODO: Pass in the selected apps, then we can figure out whether they have changed.
public void onAppsSelected(Set<String> selectedApps) {
if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) {
updateSwappableAppsTask = new UpdateAsyncTask(this, selectedApps);
updateSwappableAppsTask.execute();
} else {
showJoinWifi();
}
}
/**
* Once the UpdateAsyncTask has finished preparing our repository index, we can
* show the next screen to the user.
*/
private void onLocalRepoPrepared() {
updateSwappableAppsTask = null;
hasPreparedLocalRepo = true;
showJoinWifi();
}
private void showJoinWifi() {
inflateInnerView(R.layout.swap_join_wifi);
}
class UpdateAsyncTask extends AsyncTask<Void, String, Void> {
@SuppressWarnings("UnusedDeclaration")
private static final String TAG = "UpdateAsyncTask";
@NonNull
private final ProgressDialog progressDialog;
@NonNull
private final Set<String> selectedApps;
@NonNull
private final Uri sharingUri;
@NonNull
private final Context context;
public UpdateAsyncTask(Context c, @NonNull Set<String> apps) {
context = SwapWorkflowActivity.this.getApplicationContext();
selectedApps = apps;
progressDialog = new ProgressDialog(c);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setTitle(R.string.updating);
sharingUri = Utils.getSharingUri(FDroidApp.repo);
}
@Override
protected void onPreExecute() {
progressDialog.show();
}
@Override
protected Void doInBackground(Void... params) {
try {
final LocalRepoManager lrm = LocalRepoManager.get(context);
publishProgress(getString(R.string.deleting_repo));
lrm.deleteRepo();
for (String app : selectedApps) {
publishProgress(String.format(getString(R.string.adding_apks_format), app));
lrm.addApp(context, app);
}
lrm.writeIndexPage(sharingUri.toString());
publishProgress(getString(R.string.writing_index_jar));
lrm.writeIndexJar();
publishProgress(getString(R.string.linking_apks));
lrm.copyApksToRepo();
publishProgress(getString(R.string.copying_icons));
// run the icon copy without progress, its not a blocker
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
lrm.copyIconsToRepo();
return null;
}
}.execute();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onProgressUpdate(String... progress) {
super.onProgressUpdate(progress);
progressDialog.setMessage(progress[0]);
}
@Override
protected void onPostExecute(Void result) {
progressDialog.dismiss();
Toast.makeText(context, R.string.updated_local_repo, Toast.LENGTH_SHORT).show();
onLocalRepoPrepared();
}
}
}

View File

@ -0,0 +1,302 @@
package org.fdroid.fdroid.views.swap.views;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.support.v4.widget.CursorAdapter;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.localrepo.LocalRepoManager;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import java.util.HashSet;
public class SelectAppsView extends ListView implements
SwapWorkflowActivity.InnerView,
LoaderManager.LoaderCallbacks<Cursor>,
SearchView.OnQueryTextListener {
public SelectAppsView(Context context) {
super(context);
}
public SelectAppsView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SelectAppsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SelectAppsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private SwapWorkflowActivity getActivity() {
return (SwapWorkflowActivity)getContext();
}
private static final int LOADER_INSTALLED_APPS = 253341534;
private AppListAdapter adapter;
private String mCurrentFilterString;
private final Presenter presenter = new Presenter();
public static class Presenter {
private SelectAppsView view;
public void setView(@NonNull SelectAppsView view) {
this.view = view;
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
adapter = new AppListAdapter(this, getContext(),
getContext().getContentResolver().query(InstalledAppProvider.getContentUri(), InstalledAppProvider.DataColumns.ALL, null, null, null));
setAdapter(adapter);
addHeaderView(inflate(getContext(), R.layout.swap_create_header, null), null, false);
setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
// either reconnect with an existing loader or start a new one
getActivity().getSupportLoaderManager().initLoader(LOADER_INSTALLED_APPS, null, this);
// build list of existing apps from what is on the file system
if (FDroidApp.selectedApps == null) {
FDroidApp.selectedApps = new HashSet<>();
for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) {
if (filename.matches(".*\\.apk")) {
String packageName = filename.substring(0, filename.indexOf("_"));
FDroidApp.selectedApps.add(packageName);
}
}
}
setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
if (position > 0) {
// Ignore the headerView at position 0.
toggleAppSelected(position);
}
}
});
presenter.setView(this);
}
@Override
public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.swap_next_search, menu);
MenuItem nextMenuItem = menu.findItem(R.id.action_next);
int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT;
MenuItemCompat.setShowAsAction(nextMenuItem, flags);
nextMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
getActivity().onAppsSelected(FDroidApp.selectedApps);
return true;
}
});
SearchView searchView = new SearchView(getActivity());
MenuItem searchMenuItem = menu.findItem(R.id.action_search);
MenuItemCompat.setActionView(searchMenuItem, searchView);
MenuItemCompat.setShowAsAction(searchMenuItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
searchView.setOnQueryTextListener(this);
return true;
}
private void toggleAppSelected(int position) {
Cursor c = (Cursor) adapter.getItem(position - 1);
String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
if (FDroidApp.selectedApps.contains(packageName)) {
FDroidApp.selectedApps.remove(packageName);
} else {
FDroidApp.selectedApps.add(packageName);
}
}
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
Uri uri;
if (TextUtils.isEmpty(mCurrentFilterString)) {
uri = InstalledAppProvider.getContentUri();
} else {
uri = InstalledAppProvider.getSearchUri(mCurrentFilterString);
}
return new CursorLoader(
getActivity(),
uri,
InstalledAppProvider.DataColumns.ALL,
null,
null,
InstalledAppProvider.DataColumns.APPLICATION_LABEL);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor);
String fdroid = loader.getContext().getPackageName();
for (int i = 0; i < getCount() - 1; i++) {
Cursor c = ((Cursor) getItemAtPosition(i + 1));
String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
if (TextUtils.equals(packageName, fdroid)) {
setItemChecked(i + 1, true); // always include FDroid
} else {
for (String selected : FDroidApp.selectedApps) {
if (TextUtils.equals(packageName, selected)) {
setItemChecked(i + 1, true);
}
}
}
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
@Override
public boolean onQueryTextChange(String newText) {
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
if (mCurrentFilterString == null && newFilter == null) {
return true;
}
if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
return true;
}
mCurrentFilterString = newFilter;
getActivity().getSupportLoaderManager().restartLoader(LOADER_INSTALLED_APPS, null, this);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
// this is not needed since we respond to every change in text
return true;
}
private class AppListAdapter extends CursorAdapter {
@SuppressWarnings("UnusedDeclaration")
private static final String TAG = "AppListAdapter";
@Nullable
private LayoutInflater inflater;
@Nullable
private Drawable defaultAppIcon;
@NonNull
private final ListView listView;
public AppListAdapter(@NonNull ListView listView, @NonNull Context context, @Nullable Cursor c) {
super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
this.listView = listView;
}
@NonNull
private LayoutInflater getInflater(Context context) {
if (inflater == null) {
Context themedContext = new ContextThemeWrapper(context, R.style.SwapTheme_AppList_ListItem);
inflater = (LayoutInflater)themedContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
return inflater;
}
private Drawable getDefaultAppIcon(Context context) {
if (defaultAppIcon == null) {
defaultAppIcon = context.getResources().getDrawable(android.R.drawable.sym_def_app_icon);
}
return defaultAppIcon;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = getInflater(context).inflate(R.layout.select_local_apps_list_item, parent, false);
bindView(view, context, cursor);
return view;
}
@Override
public void bindView(final View view, final Context context, final Cursor cursor) {
TextView packageView = (TextView)view.findViewById(R.id.package_name);
TextView labelView = (TextView)view.findViewById(R.id.application_label);
ImageView iconView = (ImageView)view.findViewById(android.R.id.icon);
String packageName = cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
String appLabel = cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APPLICATION_LABEL));
Drawable icon;
try {
icon = context.getPackageManager().getApplicationIcon(packageName);
} catch (PackageManager.NameNotFoundException e) {
icon = getDefaultAppIcon(context);
}
packageView.setText(packageName);
labelView.setText(appLabel);
iconView.setImageDrawable(icon);
// Since v11, the Android SDK provided the ability to show selected list items
// by highlighting their background. Prior to this, we need to handle this ourselves
// by adding a checkbox which can toggle selected items.
View checkBoxView = view.findViewById(R.id.checkbox);
if (checkBoxView != null) {
CheckBox checkBox = (CheckBox)checkBoxView;
checkBox.setOnCheckedChangeListener(null);
final int listPosition = cursor.getPosition() + 1; // To account for the header view.
checkBox.setChecked(listView.isItemChecked(listPosition));
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listView.setItemChecked(listPosition, isChecked);
toggleAppSelected(listPosition);
}
});
}
}
}
}

View File

@ -3,18 +3,18 @@ package org.fdroid.fdroid.views.swap.views;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.widget.LinearLayout;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.swap.SwapActivity;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import java.util.Set;
public class StartSwapView extends LinearLayout {
public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.InnerView {
public StartSwapView(Context context) {
super(context);
@ -36,9 +36,9 @@ public class StartSwapView extends LinearLayout {
private SwapActivity getActivity() {
private SwapWorkflowActivity getActivity() {
// TODO: Try and find a better way to get to the SwapActivity, which makes less asumptions.
return (SwapActivity)((ContextThemeWrapper)getContext()).getBaseContext();
return (SwapWorkflowActivity)getContext();
}
@Override
@ -54,4 +54,8 @@ public class StartSwapView extends LinearLayout {
}
@Override
public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
return false;
}
}