diff options
Diffstat (limited to 'app/src')
-rw-r--r-- | app/src/main/java/org/connectbot/PubkeyListActivity.java | 532 | ||||
-rw-r--r-- | app/src/main/res/layout/act_pubkeylist.xml | 9 | ||||
-rw-r--r-- | app/src/main/res/layout/item_pubkey.xml | 2 |
3 files changed, 321 insertions, 222 deletions
diff --git a/app/src/main/java/org/connectbot/PubkeyListActivity.java b/app/src/main/java/org/connectbot/PubkeyListActivity.java index d8a8e70..97ef434 100644 --- a/app/src/main/java/org/connectbot/PubkeyListActivity.java +++ b/app/src/main/java/org/connectbot/PubkeyListActivity.java @@ -38,7 +38,6 @@ import org.connectbot.util.PubkeyUtils; import org.openintents.intents.FileManagerIntents; import android.app.AlertDialog; -import android.app.ListActivity; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; @@ -46,10 +45,18 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.ServiceConnection; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.app.AppCompatActivity; import android.text.ClipboardManager; import android.util.Log; import android.view.ContextMenu; @@ -60,8 +67,6 @@ import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageView; import android.widget.TableRow; @@ -78,7 +83,7 @@ import com.trilead.ssh2.crypto.PEMStructure; * * @author Kenny Root */ -public class PubkeyListActivity extends ListActivity implements EventListener { +public class PubkeyListActivity extends AppCompatActivity implements EventListener { public final static String TAG = "CB.PubkeyListActivity"; private static final int MAX_KEYFILE_SIZE = 8192; @@ -94,6 +99,10 @@ public class PubkeyListActivity extends ListActivity implements EventListener { protected LayoutInflater inflater = null; + private View mEmptyView; + private RecyclerView mPubkeyListView; + private PubkeyAdapter mAdapter; + protected TerminalManager bound = null; private MenuItem onstartToggle = null; @@ -134,23 +143,14 @@ public class PubkeyListActivity extends ListActivity implements EventListener { super.onCreate(icicle); setContentView(R.layout.act_pubkeylist); - registerForContextMenu(getListView()); - - getListView().setOnItemClickListener(new OnItemClickListener() { - public void onItemClick(AdapterView<?> adapter, View view, int position, long id) { - PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(position); - boolean loaded = bound != null && bound.isKeyLoaded(pubkey.getNickname()); + mPubkeyListView = (RecyclerView) findViewById(R.id.list); + mPubkeyListView.setHasFixedSize(true); + mPubkeyListView.setLayoutManager(new LinearLayoutManager(this)); + mPubkeyListView.addItemDecoration(new PubkeyListItemDecoration(this)); - // handle toggling key in-memory on/off - if (loaded) { - bound.removeKey(pubkey.getNickname()); - updateList(); - } else { - handleAddKey(pubkey); - } + mEmptyView = findViewById(R.id.empty); - } - }); + registerForContextMenu(mPubkeyListView); clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); @@ -278,185 +278,26 @@ public class PubkeyListActivity extends ListActivity implements EventListener { updateList(); } - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - // Create menu to handle deleting and editing pubkey - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; - final PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(info.position); - - menu.setHeaderTitle(pubkey.getNickname()); - - // TODO: option load/unload key from in-memory list - // prompt for password as needed for passworded keys - - // cant change password or clipboard imported keys - final boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); - final boolean loaded = bound != null && bound.isKeyLoaded(pubkey.getNickname()); - - MenuItem load = menu.add(loaded ? R.string.pubkey_memory_unload : R.string.pubkey_memory_load); - load.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - if (loaded) { - bound.removeKey(pubkey.getNickname()); - updateList(); - } else { - handleAddKey(pubkey); - //bound.addKey(nickname, trileadKey); - } - return true; - } - }); - - onstartToggle = menu.add(R.string.pubkey_load_on_start); - onstartToggle.setEnabled(!pubkey.isEncrypted()); - onstartToggle.setCheckable(true); - onstartToggle.setChecked(pubkey.isStartup()); - onstartToggle.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - // toggle onstart status - pubkey.setStartup(!pubkey.isStartup()); - PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); - pubkeyDb.savePubkey(pubkey); - updateList(); - return true; - } - }); - MenuItem copyPublicToClipboard = menu.add(R.string.pubkey_copy_public); - copyPublicToClipboard.setEnabled(!imported); - copyPublicToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - try { - PublicKey pk = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); - String openSSHPubkey = PubkeyUtils.convertToOpenSSHFormat(pk, pubkey.getNickname()); - - clipboard.setText(openSSHPubkey); - } catch (Exception e) { - e.printStackTrace(); - } - return true; - } - }); - - MenuItem copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private); - copyPrivateToClipboard.setEnabled(!pubkey.isEncrypted() || imported); - copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - try { - String data = null; - - if (imported) - data = new String(pubkey.getPrivateKey()); - else { - PrivateKey pk = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType()); - data = PubkeyUtils.exportPEM(pk, null); - } - - clipboard.setText(data); - } catch (Exception e) { - e.printStackTrace(); - } - return true; - } - }); - - MenuItem changePassword = menu.add(R.string.pubkey_change_password); - changePassword.setEnabled(!imported); - changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - final View changePasswordView = inflater.inflate(R.layout.dia_changepassword, null, false); - ((TableRow) changePasswordView.findViewById(R.id.old_password_prompt)) - .setVisibility(pubkey.isEncrypted() ? View.VISIBLE : View.GONE); - new AlertDialog.Builder(PubkeyListActivity.this) - .setView(changePasswordView) - .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - String oldPassword = ((EditText) changePasswordView.findViewById(R.id.old_password)).getText().toString(); - String password1 = ((EditText) changePasswordView.findViewById(R.id.password1)).getText().toString(); - String password2 = ((EditText) changePasswordView.findViewById(R.id.password2)).getText().toString(); - - if (!password1.equals(password2)) { - new AlertDialog.Builder(PubkeyListActivity.this) - .setMessage(R.string.alert_passwords_do_not_match_msg) - .setPositiveButton(android.R.string.ok, null) - .create().show(); - return; - } - - try { - if (!pubkey.changePassword(oldPassword, password1)) - new AlertDialog.Builder(PubkeyListActivity.this) - .setMessage(R.string.alert_wrong_password_msg) - .setPositiveButton(android.R.string.ok, null) - .create().show(); - else { - PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); - pubkeyDb.savePubkey(pubkey); - updateList(); - } - } catch (Exception e) { - Log.e(TAG, "Could not change private key password", e); - new AlertDialog.Builder(PubkeyListActivity.this) - .setMessage(R.string.alert_key_corrupted_msg) - .setPositiveButton(android.R.string.ok, null) - .create().show(); - } - } - }) - .setNegativeButton(android.R.string.cancel, null).create().show(); - - return true; - } - }); - - confirmUse = menu.add(R.string.pubkey_confirm_use); - confirmUse.setCheckable(true); - confirmUse.setChecked(pubkey.isConfirmUse()); - confirmUse.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - // toggle confirm use - pubkey.setConfirmUse(!pubkey.isConfirmUse()); - PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); - pubkeyDb.savePubkey(pubkey); - updateList(); - return true; - } - }); - - MenuItem delete = menu.add(R.string.pubkey_delete); - delete.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - // prompt user to make sure they really want this - new AlertDialog.Builder(PubkeyListActivity.this) - .setMessage(getString(R.string.delete_message, pubkey.getNickname())) - .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - - // dont forget to remove from in-memory - if (loaded) { - bound.removeKey(pubkey.getNickname()); - } - - // delete from backend database and update gui - PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); - pubkeyDb.deletePubkey(pubkey); - updateList(); - } - }) - .setNegativeButton(R.string.delete_neg, null).create().show(); - - return true; - } - }); - - } protected void updateList() { PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); pubkeys = pubkeyDb.allPubkeys(); - PubkeyAdapter adapter = new PubkeyAdapter(this, pubkeys); - this.setListAdapter(adapter); + mAdapter = new PubkeyAdapter(this, pubkeys); + mPubkeyListView.setAdapter(mAdapter); + adjustViewVisibility(); + } + + /** + * If the pubkey list is empty, hides the list and shows the empty message; otherwise, shows + * the list and hides the empty message. + */ + // TODO: this method is being duplicated but could be part of a superclass + private void adjustViewVisibility() { + boolean isEmpty = mAdapter.getItemCount() == 0; + mPubkeyListView.setVisibility(isEmpty ? View.GONE : View.VISIBLE); + mEmptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE); } @Override @@ -589,39 +430,241 @@ public class PubkeyListActivity extends ListActivity implements EventListener { .setNegativeButton(android.R.string.cancel, null).create().show(); } - class PubkeyAdapter extends ArrayAdapter<PubkeyBean> { - private List<PubkeyBean> pubkeys; + @VisibleForTesting + protected class PubkeyAdapter extends RecyclerView.Adapter<PubkeyAdapter.ViewHolder> { + private final LayoutInflater inflater; + private final List<PubkeyBean> pubkeys; + private final Context context; + + // KLUDGE: duplication of HostListActivity? + class ViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, View.OnCreateContextMenuListener { + public final ImageView icon; + public final TextView nickname; + public final TextView caption; + public PubkeyBean pubkey; + + public ViewHolder(View v) { + super(v); + v.setOnClickListener(this); + v.setOnCreateContextMenuListener(this); + + icon = (ImageView) v.findViewById(android.R.id.icon); + nickname = (TextView) v.findViewById(android.R.id.text1); + caption = (TextView) v.findViewById(android.R.id.text2); + } - class ViewHolder { - public TextView nickname; - public TextView caption; - public ImageView icon; + @Override + public void onClick(View v) { + boolean loaded = bound != null && bound.isKeyLoaded(pubkey.getNickname()); + + // handle toggling key in-memory on/off + if (loaded) { + bound.removeKey(pubkey.getNickname()); + updateList(); + } else { + handleAddKey(pubkey); + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + // Create menu to handle deleting and editing pubkey + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + + menu.setHeaderTitle(pubkey.getNickname()); + + // TODO: option load/unload key from in-memory list + // prompt for password as needed for passworded keys + + // cant change password or clipboard imported keys + final boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); + final boolean loaded = bound != null && bound.isKeyLoaded(pubkey.getNickname()); + + MenuItem load = menu.add(loaded ? R.string.pubkey_memory_unload : R.string.pubkey_memory_load); + load.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + if (loaded) { + bound.removeKey(pubkey.getNickname()); + updateList(); + } else { + handleAddKey(pubkey); + //bound.addKey(nickname, trileadKey); + } + return true; + } + }); + + onstartToggle = menu.add(R.string.pubkey_load_on_start); + onstartToggle.setEnabled(!pubkey.isEncrypted()); + onstartToggle.setCheckable(true); + onstartToggle.setChecked(pubkey.isStartup()); + onstartToggle.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // toggle onstart status + pubkey.setStartup(!pubkey.isStartup()); + PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); + pubkeyDb.savePubkey(pubkey); + updateList(); + return true; + } + }); + + MenuItem copyPublicToClipboard = menu.add(R.string.pubkey_copy_public); + copyPublicToClipboard.setEnabled(!imported); + copyPublicToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + try { + PublicKey pk = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); + String openSSHPubkey = PubkeyUtils.convertToOpenSSHFormat(pk, pubkey.getNickname()); + + clipboard.setText(openSSHPubkey); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + }); + + MenuItem copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private); + copyPrivateToClipboard.setEnabled(!pubkey.isEncrypted() || imported); + copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + try { + String data = null; + + if (imported) + data = new String(pubkey.getPrivateKey()); + else { + PrivateKey pk = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType()); + data = PubkeyUtils.exportPEM(pk, null); + } + + clipboard.setText(data); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + }); + + MenuItem changePassword = menu.add(R.string.pubkey_change_password); + changePassword.setEnabled(!imported); + changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + final View changePasswordView = inflater.inflate(R.layout.dia_changepassword, null, false); + ((TableRow) changePasswordView.findViewById(R.id.old_password_prompt)) + .setVisibility(pubkey.isEncrypted() ? View.VISIBLE : View.GONE); + new AlertDialog.Builder(PubkeyListActivity.this) + .setView(changePasswordView) + .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String oldPassword = ((EditText) changePasswordView.findViewById(R.id.old_password)).getText().toString(); + String password1 = ((EditText) changePasswordView.findViewById(R.id.password1)).getText().toString(); + String password2 = ((EditText) changePasswordView.findViewById(R.id.password2)).getText().toString(); + + if (!password1.equals(password2)) { + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(R.string.alert_passwords_do_not_match_msg) + .setPositiveButton(android.R.string.ok, null) + .create().show(); + return; + } + + try { + if (!pubkey.changePassword(oldPassword, password1)) + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(R.string.alert_wrong_password_msg) + .setPositiveButton(android.R.string.ok, null) + .create().show(); + else { + PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); + pubkeyDb.savePubkey(pubkey); + updateList(); + } + } catch (Exception e) { + Log.e(TAG, "Could not change private key password", e); + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(R.string.alert_key_corrupted_msg) + .setPositiveButton(android.R.string.ok, null) + .create().show(); + } + } + }) + .setNegativeButton(android.R.string.cancel, null).create().show(); + + return true; + } + }); + + confirmUse = menu.add(R.string.pubkey_confirm_use); + confirmUse.setCheckable(true); + confirmUse.setChecked(pubkey.isConfirmUse()); + confirmUse.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // toggle confirm use + pubkey.setConfirmUse(!pubkey.isConfirmUse()); + PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); + pubkeyDb.savePubkey(pubkey); + updateList(); + return true; + } + }); + + MenuItem delete = menu.add(R.string.pubkey_delete); + delete.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // prompt user to make sure they really want this + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(getString(R.string.delete_message, pubkey.getNickname())) + .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + + // dont forget to remove from in-memory + if (loaded) { + bound.removeKey(pubkey.getNickname()); + } + + // delete from backend database and update gui + PubkeyDatabase pubkeyDb = PubkeyDatabase.get(PubkeyListActivity.this); + pubkeyDb.deletePubkey(pubkey); + updateList(); + } + }) + .setNegativeButton(R.string.delete_neg, null).create().show(); + + return true; + } + }); + } } public PubkeyAdapter(Context context, List<PubkeyBean> pubkeys) { - super(context, R.layout.item_pubkey, pubkeys); - + this.context = context; this.pubkeys = pubkeys; + this.inflater = LayoutInflater.from(context); } @Override - public View getView(int position, View convertView, ViewGroup parent) { - ViewHolder holder; - - if (convertView == null) { - convertView = inflater.inflate(R.layout.item_pubkey, null, false); - - holder = new ViewHolder(); + public PubkeyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_pubkey, parent, false); + ViewHolder vh = new ViewHolder(v); + return vh; + } - holder.nickname = (TextView) convertView.findViewById(android.R.id.text1); - holder.caption = (TextView) convertView.findViewById(android.R.id.text2); - holder.icon = (ImageView) convertView.findViewById(android.R.id.icon1); + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + PubkeyBean pubkey = pubkeys.get(position); + if (pubkey == null) { + // Well, something bad happened. We can't continue. + Log.e("PubkeyAdapter", "Pubkey bean is null!"); - convertView.setTag(holder); - } else - holder = (ViewHolder) convertView.getTag(); + holder.nickname.setText("Error during lookup"); + holder.caption.setText("see 'adb logcat' for more"); + } + holder.pubkey = pubkey; - PubkeyBean pubkey = pubkeys.get(position); holder.nickname.setText(pubkey.getNickname()); boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); @@ -651,10 +694,67 @@ public class PubkeyListActivity extends ListActivity implements EventListener { if (bound.isKeyLoaded(pubkey.getNickname())) holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true); else - holder.icon.setImageState(new int[] { }, true); + holder.icon.setImageState(new int[] { }, true); } + } - return convertView; + public PubkeyBean getItem(int position) { + return pubkeys.get(position); + } + + @Override + public long getItemId(int position) { + return pubkeys.get(position).getId(); + } + + @Override + public int getItemCount() { + return pubkeys.size(); + } + } + + /** + * Item decorations for pubkey list items, which adds a divider between items and leaves a + * small offset at the top of the list to adhere to the Material Design spec. + */ + // KLUDGE: duplication + private class PubkeyListItemDecoration extends RecyclerView.ItemDecoration { + private final int[] ATTRS = new int[]{ + android.R.attr.listDivider + }; + + private final int TOP_LIST_OFFSET = 8; + + private Drawable mDivider; + + public PubkeyListItemDecoration(Context c) { + final TypedArray a = c.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + final int left = parent.getPaddingLeft(); + final int right = parent.getWidth() - parent.getPaddingRight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = + (RecyclerView.LayoutParams) child.getLayoutParams(); + final int top = child.getBottom() + params.bottomMargin; + final int bottom = top + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + int top = parent.getChildAdapterPosition(view) == 0 ? TOP_LIST_OFFSET : 0; + outRect.set(0, top, 0, mDivider.getIntrinsicHeight()); } } } diff --git a/app/src/main/res/layout/act_pubkeylist.xml b/app/src/main/res/layout/act_pubkeylist.xml index 0737d05..62b13eb 100644 --- a/app/src/main/res/layout/act_pubkeylist.xml +++ b/app/src/main/res/layout/act_pubkeylist.xml @@ -24,15 +24,14 @@ android:orientation="vertical" > - <ListView - android:id="@android:id/list" + <android.support.v7.widget.RecyclerView + android:id="@+id/list" android:layout_width="fill_parent" - android:layout_height="0dip" - android:layout_weight="1" + android:layout_height="fill_parent" /> <TextView - android:id="@android:id/empty" + android:id="@+id/empty" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" diff --git a/app/src/main/res/layout/item_pubkey.xml b/app/src/main/res/layout/item_pubkey.xml index 2dce26b..6f63af9 100644 --- a/app/src/main/res/layout/item_pubkey.xml +++ b/app/src/main/res/layout/item_pubkey.xml @@ -26,7 +26,7 @@ android:padding="10dip"> <ImageView - android:id="@android:id/icon1" + android:id="@android:id/icon" android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_alignParentEnd="true" |