diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesAdapter.java b/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesAdapter.java index 4abfef6..cb3f36f 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesAdapter.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesAdapter.java @@ -2,34 +2,25 @@ import android.database.Cursor; import android.graphics.Typeface; -import androidx.recyclerview.widget.RecyclerView; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + import com.crossbowffs.nekosms.R; import com.crossbowffs.nekosms.data.SmsMessageData; import com.crossbowffs.nekosms.loader.BlockedSmsLoader; import com.crossbowffs.nekosms.widget.RecyclerCursorAdapter; -/* package */ class BlockedMessagesAdapter extends RecyclerCursorAdapter { - public class BlockedSmsItemHolder extends RecyclerView.ViewHolder { - public final TextView mSenderTextView; - public final TextView mTimeSentTextView; - public final TextView mBodyTextView; - public SmsMessageData mMessageData; - - public BlockedSmsItemHolder(View itemView) { - super(itemView); +import java.util.Set; - mSenderTextView = (TextView)itemView.findViewById(R.id.blocked_message_sender_textview); - mTimeSentTextView = (TextView)itemView.findViewById(R.id.blocked_message_time_sent_textview); - mBodyTextView = (TextView)itemView.findViewById(R.id.blocked_message_body_textview); - } - } +/* package */ class BlockedMessagesAdapter extends RecyclerCursorAdapter { private final BlockedMessagesFragment mFragment; + private OnItemClickListener onItemClickListener; public BlockedMessagesAdapter(BlockedMessagesFragment fragment) { mFragment = fragment; @@ -48,16 +39,18 @@ protected int[] onBindColumns(Cursor cursor) { } @Override - public void onBindViewHolder(BlockedSmsItemHolder holder, Cursor cursor) { + public void onBindViewHolder(BlockedSmsItemHolder holder,Cursor cursor) { + Set selectedMsgIds = mFragment.getSelectedMsgIds(); final SmsMessageData messageData = BlockedSmsLoader.get().getData(cursor, getColumns(), holder.mMessageData); holder.mMessageData = messageData; + int subid = messageData.getSubId(); String sender = messageData.getSender(); long timeSent = messageData.getTimeSent(); String body = messageData.getBody(); CharSequence timeSentString = DateUtils.getRelativeTimeSpanString(mFragment.getContext(), timeSent); - holder.mSenderTextView.setText(sender); + holder.mSenderTextView.setText(sender+" (SIM "+subid+")"); holder.mTimeSentTextView.setText(timeSentString); holder.mBodyTextView.setText(body); if (messageData.isRead()) { @@ -65,16 +58,55 @@ public void onBindViewHolder(BlockedSmsItemHolder holder, Cursor cursor) { holder.mTimeSentTextView.setTypeface(null, Typeface.NORMAL); holder.mBodyTextView.setTypeface(null, Typeface.NORMAL); } else { - holder.mSenderTextView.setTypeface(null, Typeface.BOLD); - holder.mTimeSentTextView.setTypeface(null, Typeface.BOLD); - holder.mBodyTextView.setTypeface(null, Typeface.BOLD); + holder.mSenderTextView.setTypeface(null, Typeface.BOLD_ITALIC); + holder.mTimeSentTextView.setTypeface(null, Typeface.BOLD_ITALIC); + holder.mBodyTextView.setTypeface(null, Typeface.BOLD_ITALIC); + } + + if (selectedMsgIds.contains(messageData.getId())) { + holder.itemView.setAlpha(0.2f); + } else { + holder.itemView.setAlpha(1.0f); + } + + if (onItemClickListener != null) { + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onItemClickListener.onItemClick(v, messageData); + } + }); + holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + //return: true:不继续传递事件,告诉系统此事件已被处理;false:继续传递事件,上级可继续处理 + return onItemClickListener.onItemLongClick(v, messageData); + } + }); } + } + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + this.onItemClickListener = onItemClickListener; + } + + public interface OnItemClickListener { + void onItemClick(View view, SmsMessageData messageData); + boolean onItemLongClick(View view, SmsMessageData messageData); + } + + public class BlockedSmsItemHolder extends RecyclerView.ViewHolder { + public final TextView mSenderTextView; + public final TextView mTimeSentTextView; + public final TextView mBodyTextView; + public SmsMessageData mMessageData; - holder.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mFragment.showMessageDetailsDialog(messageData); - } - }); + public BlockedSmsItemHolder(View itemView) { + super(itemView); + mSenderTextView = (TextView) itemView.findViewById(R.id.blocked_message_sender_textview); + mTimeSentTextView = (TextView) itemView.findViewById(R.id.blocked_message_time_sent_textview); + mBodyTextView = (TextView) itemView.findViewById(R.id.blocked_message_body_textview); + } } + } diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesFragment.java b/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesFragment.java index 0aeb8af..b783c8c 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesFragment.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/BlockedMessagesFragment.java @@ -1,18 +1,40 @@ package com.crossbowffs.nekosms.app; +import android.annotation.SuppressLint; import android.app.LoaderManager; -import android.content.*; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.CursorLoader; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.Loader; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.LinearLayoutManager; import android.text.Html; +import android.text.InputType; import android.text.Spanned; import android.text.format.DateUtils; -import android.view.*; -import com.crossbowffs.nekosms.BuildConfig; +import android.text.method.LinkMovementMethod; +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.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.ActionMode; +import androidx.appcompat.widget.SearchView; +import androidx.recyclerview.widget.LinearLayoutManager; + import com.crossbowffs.nekosms.R; import com.crossbowffs.nekosms.consts.BroadcastConsts; import com.crossbowffs.nekosms.data.SmsMessageData; @@ -20,28 +42,60 @@ import com.crossbowffs.nekosms.loader.DatabaseException; import com.crossbowffs.nekosms.loader.InboxSmsLoader; import com.crossbowffs.nekosms.provider.DatabaseContract; +import com.crossbowffs.nekosms.utils.MapUtils; import com.crossbowffs.nekosms.utils.Xlog; import com.crossbowffs.nekosms.utils.XposedUtils; import com.crossbowffs.nekosms.widget.ListRecyclerView; -public class BlockedMessagesFragment extends MainFragment implements LoaderManager.LoaderCallbacks { - private static final boolean DEBUG_MODE = BuildConfig.DEBUG; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +public class BlockedMessagesFragment extends MainFragment implements LoaderManager.LoaderCallbacks , ActionMode.Callback { public static final String ARG_MESSAGE_URI = "message_uri"; private ListRecyclerView mRecyclerView; private View mEmptyView; private BlockedMessagesAdapter mAdapter; + private ActionMode actionMode; + private SearchView searchView; + private Set selectedMsgIds = new HashSet<>(); + + public ActionMode getActionMode() { + return actionMode; + } + + public Set getSelectedMsgIds() { + return selectedMsgIds; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } + @Override + public void onDetach() { + super.onDetach(); + } + + @Override + public void onDestroy() { + if(actionMode!=null){ + actionMode.finish(); + } + super.onDestroy(); + } + @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_blocked_messages, container, false); - mRecyclerView = (ListRecyclerView)view.findViewById(R.id.blocked_messages_recyclerview); + mRecyclerView = view.findViewById(R.id.blocked_messages_recyclerview); mEmptyView = view.findViewById(android.R.id.empty); return view; } @@ -55,11 +109,47 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { mRecyclerView.setAdapter(mAdapter); mRecyclerView.setEmptyView(mEmptyView); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - registerForContextMenu(mRecyclerView); + //registerForContextMenu(mRecyclerView); disableFab(); - setTitle(R.string.blocked_messages); + setTitle(R.string.blocked_messages_whith_num,mAdapter.getItemCount()); onNewArguments(getArguments()); BlockedSmsLoader.get().markAllSeen(getContext()); + + mAdapter.setOnItemClickListener(new BlockedMessagesAdapter.OnItemClickListener() { + @Override + public void onItemClick(View view, SmsMessageData msg_data) { + if (actionMode != null) { + addOrRemove(msg_data.getId());// 如果处于多选状态,则进入多选状态的逻辑 + } else { + showMessageDetailsDialog(msg_data);// 如果不是多选状态,则展示信息详情 + } + } + + @Override + public boolean onItemLongClick(View view, SmsMessageData msg_data) { + if (actionMode == null) { + actionMode = getMainActivity().startSupportActionMode(BlockedMessagesFragment.this); + } + return false; + } + }); + } + + private void addOrRemove(long msgid) { + // 如果包含,则取消选择;如果不包含,则添加选择 + if (selectedMsgIds.contains(msgid)) { + selectedMsgIds.remove(msgid); + } else { + selectedMsgIds.add(msgid); + } + + // 如果没有选中任何的item,则退出多选模式;否则更新条目背景 + if (selectedMsgIds.size() == 0) { + actionMode.finish(); + } else { + actionMode.setTitle(getString(R.string.message_multi_selected,selectedMsgIds.size())); + mAdapter.notifyDataSetChanged(); + } } @Override @@ -77,62 +167,89 @@ public void onNewArguments(Bundle args) { } @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - MenuInflater inflater = getActivity().getMenuInflater(); - inflater.inflate(R.menu.context_blocked_messages, menu); - menu.setHeaderTitle(R.string.message_actions); - } + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.options_blocked_messages, menu); + inflater.inflate(R.menu.options_debug, menu); + searchView = (SearchView) menu.findItem(R.id.app_bar_search).getActionView(); - @Override - public boolean onContextItemSelected(MenuItem item) { - ListRecyclerView.ContextMenuInfo info = (ListRecyclerView.ContextMenuInfo)item.getMenuInfo(); - switch (item.getItemId()) { - case R.id.menu_item_restore_message: - restoreSms(info.mId); - return true; - case R.id.menu_item_delete_message: - deleteSms(info.mId); - return true; - default: - return super.onContextItemSelected(item); - } + searchView.setMaxWidth(500); + searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + refreshList(); + //点击提交后,隐藏输入法 + InputMethodManager imm = (InputMethodManager) getMainActivity().getSystemService((Context.INPUT_METHOD_SERVICE)); + if (imm != null) { + imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0); + searchView.clearFocus(); + } + return true; + } + + @Override + public boolean onQueryTextChange(String query) { + refreshList(); + return true; + } + }); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.options_blocked_messages, menu); - if (DEBUG_MODE) { - inflater.inflate(R.menu.options_debug, menu); + //通过restartLoader 刷新列表,通过bundle传递查询条件 + private void refreshList() { + if(getContext()==null)return; + CharSequence query=""; + if(searchView!=null){ + query=searchView.getQuery(); } + + Bundle bundle=new Bundle(); + bundle.putCharSequence("query", query); + getLoaderManager().restartLoader(0, bundle, this); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.menu_item_create_test: - createTestSms(); - return true; - case R.id.menu_item_clear_blocked: - showConfirmClearDialog(); - return true; - default: - return super.onOptionsItemSelected(item); + case R.id.menu_item_create_test: + showCreateTestSmsDialog(); + return true; + case R.id.menu_item_clear_blocked: + showConfirmClearDialog(); + return true; + default: + return super.onOptionsItemSelected(item); } } @Override public Loader onCreateLoader(int id, Bundle args) { - return new CursorLoader( - getContext(), - DatabaseContract.BlockedMessages.CONTENT_URI, - DatabaseContract.BlockedMessages.ALL, null, null, - DatabaseContract.BlockedMessages.TIME_SENT + " DESC" - ); + if(args==null){//默认查全部 + return new CursorLoader( + getContext(), + DatabaseContract.BlockedMessages.CONTENT_URI, + DatabaseContract.BlockedMessages.ALL, + null, + null, + DatabaseContract.BlockedMessages.TIME_SENT + " DESC" + ); + }else{//searchView!=null时,根据输入条件来查询。其中query==""时,也是查全部 + CharSequence query=args.getCharSequence("query"); + return new CursorLoader( + getContext(), + DatabaseContract.BlockedMessages.CONTENT_URI, + DatabaseContract.BlockedMessages.ALL, + DatabaseContract.BlockedMessages.BODY + " LIKE ? OR " + DatabaseContract.BlockedMessages.SENDER + " LIKE ?", + new String[]{"%" + query + "%", "%" + query + "%"}, + DatabaseContract.BlockedMessages.TIME_SENT + " DESC" + ); + } } @Override public void onLoadFinished(Loader loader, Cursor data) { mAdapter.changeCursor(data); + setTitle(R.string.blocked_messages_whith_num,data.getCount()); } @Override @@ -140,15 +257,6 @@ public void onLoaderReset(Loader loader) { mAdapter.changeCursor(null); } - private void clearAllMessages() { - Context context = getContext(); - if (context == null) return; - - NotificationHelper.cancelAllNotifications(context); - BlockedSmsLoader.get().deleteAll(context); - showSnackbar(R.string.cleared_blocked_messages); - } - private void showConfirmClearDialog() { Context context = getContext(); if (context == null) return; @@ -160,13 +268,70 @@ private void showConfirmClearDialog() { .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - clearAllMessages(); + //删除当前页面展示的所有信息 + deleteSms(mAdapter.getAllItemId()); } }) .setNegativeButton(R.string.cancel, null) .show(); } + private void showCreateTestSmsDialog() { + Context context = getContext(); + if (context == null) return; + + //build view + final EditText et_sender = new EditText(context); + final EditText et_body = new EditText(context); + final EditText et_subid = new EditText(context); + final EditText et_timesend = new EditText(context); + final EditText et_timereceived = new EditText(context); + + et_sender.setHint("发送者"); + et_body.setHint("内容"); + et_subid.setHint("SIM ID, 默认 1"); + et_timesend.setHint("发送时间"); + et_timereceived.setHint("接收时间"); + et_subid.setInputType(InputType.TYPE_CLASS_NUMBER); + + LinearLayout.LayoutParams llp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + llp.setMargins(0,50,0,0); + et_sender.setLayoutParams(llp); + et_body.setLayoutParams(llp); + et_subid.setLayoutParams(llp); + et_timesend.setLayoutParams(llp); + et_timereceived.setLayoutParams(llp); + + LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + ll.setPadding(50,50,50,50); + ll.addView(et_sender); + ll.addView(et_body); + ll.addView(et_subid); + ll.addView(et_timesend); + ll.addView(et_timereceived); + + //build data + et_sender.setText("106"+(new Random().nextInt(900_000_000)+100_000_000)); + et_body.setText("【测试】您好,验证码为"+(new Random().nextInt(900_000)+100_000)); + et_subid.setText(""+(new Random().nextInt(2)+1)); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + et_timesend.setText(simpleDateFormat.format(new Date())); + et_timereceived.setText(simpleDateFormat.format(new Date())); + + new AlertDialog.Builder(context) + .setTitle("创建测试短信") + .setView(ll) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + createTestSms(et_sender.getText().toString(),et_body.getText().toString(),et_subid.getText().toString(),et_timesend.getText().toString(),et_timereceived.getText().toString()); + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + private void showMessageDetailsDialog(Uri uri) { Context context = getContext(); SmsMessageData messageData = BlockedSmsLoader.get().query(context, uri); @@ -191,9 +356,10 @@ public void showMessageDetailsDialog(final SmsMessageData messageData) { long timeSent = messageData.getTimeSent(); String escapedBody = Html.escapeHtml(body).replace(" ", "
"); String timeSentString = DateUtils.getRelativeDateTimeString(context, timeSent, 0, DateUtils.WEEK_IN_MILLIS, 0).toString(); - Spanned html = Html.fromHtml(getString(R.string.format_message_details, sender, timeSentString, escapedBody)); + @SuppressLint("StringFormatMatches") + Spanned html = Html.fromHtml(getString(R.string.format_message_details, sender, timeSentString, escapedBody,messageData.getSubId())); - new AlertDialog.Builder(context) + AlertDialog dialog = new AlertDialog.Builder(context) .setMessage(html) .setNeutralButton(R.string.close, null) .setPositiveButton(R.string.restore, new DialogInterface.OnClickListener() { @@ -209,7 +375,10 @@ public void onClick(DialogInterface dialog, int which) { } }) .show(); - + //设置WEB链接自动识别 + TextView textView = dialog.findViewById(android.R.id.message); + textView.setAutoLinkMask(android.text.util.Linkify.WEB_URLS); + textView.setMovementMethod(LinkMovementMethod.getInstance()); BlockedSmsLoader.get().setReadStatus(context, messageData.getId(), true); } @@ -270,6 +439,41 @@ public void onClick(View v) { }); } + private void restoreSms(Set smsIds) { + Context context = getContext(); + if (context == null) return; + + int num=0; + for (long smsId : smsIds) { + // We've obviously seen the message, so remove the notification + NotificationHelper.cancelNotification(context, smsId); + + // Load message content (so we can undo) + final SmsMessageData messageData = BlockedSmsLoader.get().query(context, smsId); + if (messageData == null) { + Xlog.e("Failed to restore message: could not load data"); + continue; + } + + // Write message to the inbox + final Uri inboxSmsUri; + try { + inboxSmsUri = InboxSmsLoader.writeMessage(context, messageData); + num++; + } catch (SecurityException e) { + Xlog.e("Failed to restore message: do not have permissions to write SMS"); + continue; + } catch (DatabaseException e) { + Xlog.e("Failed to restore message: could not write to SMS inbox"); + continue; + } + + // Delete the message after we successfully write it to the inbox + BlockedSmsLoader.get().delete(context, smsId); + } + showSnackbar(getString(R.string.message_multi_restored, num)); + } + private void deleteSms(long smsId) { Context context = getContext(); if (context == null) return; @@ -295,22 +499,131 @@ public void onClick(View v) { }); } - private void createTestSms() { + private void deleteSms(Set smsIds) { + Context context = getContext(); + if (context == null) return; + + String ids_str="-1"; + for (long smsId : smsIds) { + NotificationHelper.cancelNotification(context, smsId); + ids_str=ids_str+","+smsId; + } + int num = BlockedSmsLoader.get().delete(context, DatabaseContract.BlockedMessages._ID + " in("+ids_str+")", null); + showSnackbar(getString(R.string.message_multi_deleted, num)); + } + + private void setSmsStatusToRead(Set smsIds) { Context context = getContext(); if (context == null) return; + String ids_str="-1"; + for (long smsId : smsIds) { + NotificationHelper.cancelNotification(context, smsId); + ids_str=ids_str+","+smsId; + } + + ContentValues values = MapUtils.contentValuesForSize(2); + values.put(DatabaseContract.BlockedMessages.READ, 1 ); + values.put(DatabaseContract.BlockedMessages.SEEN, 1); + //批量已读 + int num = BlockedSmsLoader.get().update(context, values,DatabaseContract.BlockedMessages._ID + " in("+ids_str+")", null); + //showSnackbar(getString(R.string.xxx, num)); + } + + private void createTestSms(String sender,String body,String str_subid,String sendDate,String receivedDate) { + Context context = getContext(); + if (context == null) return; + int subid=1; + long timeSent=System.currentTimeMillis(); + long timeReceived=timeSent; + + try{ + subid=Integer.parseInt(str_subid); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + timeSent = simpleDateFormat.parse(sendDate).getTime(); + timeReceived = simpleDateFormat.parse(receivedDate).getTime(); + } catch (NumberFormatException e){ + subid=1; + } catch (ParseException e) { + timeSent=System.currentTimeMillis(); + timeReceived=timeSent; + } SmsMessageData message = new SmsMessageData(); - message.setSender("+11234567890"); - message.setBody("Thanks for signing up for Cat Facts! You will now receive fun daily facts about CATS! >o<"); - message.setTimeReceived(System.currentTimeMillis()); - message.setTimeSent(System.currentTimeMillis()); + message.setSender(sender); + message.setBody(body); + message.setTimeSent(timeSent); + message.setTimeReceived(timeReceived); message.setRead(false); message.setSeen(false); + message.setSubId(subid); Uri uri = BlockedSmsLoader.get().insert(context, message); Intent intent = new Intent(BroadcastConsts.ACTION_RECEIVE_SMS); intent.setComponent(new ComponentName(context, BroadcastConsts.RECEIVER_NAME)); intent.putExtra(BroadcastConsts.EXTRA_MESSAGE, uri); context.sendBroadcast(intent); + + //mAdapter.notifyDataSetChanged(); + //Toast.makeText(context,"Test Sms",Toast.LENGTH_SHORT).show(); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (actionMode == null) { + actionMode = mode; + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.actionmode_blocked_messages, menu); + actionMode.setTitle(getString(R.string.message_multi_selected,selectedMsgIds.size())); + + mAdapter.notifyDataSetChanged();// 更新列表界面,否则无法显示已选的item + return true; + } else { + return false; + } + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_action_delete:// 删除选中项 + deleteSms(selectedMsgIds); + mode.finish(); + return true; + case R.id.menu_action_restore:// 恢复选中项 + restoreSms(selectedMsgIds); + mode.finish(); + return true; + case R.id.menu_action_all_select:// 全选 + selectedMsgIds=mAdapter.getAllItemId(); + actionMode.setTitle(getString(R.string.message_multi_selected,selectedMsgIds.size())); + mAdapter.notifyDataSetChanged(); + return true; + case R.id.menu_action_inv_select:// 反选 + Set allids = mAdapter.getAllItemId(); + if(allids.removeAll(selectedMsgIds)){ + selectedMsgIds =allids; + } + actionMode.setTitle(getString(R.string.message_multi_selected,selectedMsgIds.size())); + mAdapter.notifyDataSetChanged(); + return true; + case R.id.menu_action_read:// 选中项设为已读 + setSmsStatusToRead(selectedMsgIds); + mode.finish(); + return true; + default: + return false; + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + actionMode = null; + selectedMsgIds.clear(); + mAdapter.notifyDataSetChanged(); } } diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorActivity.java b/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorActivity.java index 0d77062..90adb4a 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorActivity.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorActivity.java @@ -1,72 +1,34 @@ package com.crossbowffs.nekosms.app; -import android.app.Fragment; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import com.google.android.material.tabs.TabLayout; -import androidx.core.app.ActivityCompat; -import androidx.viewpager.widget.ViewPager; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.widget.FrameLayout; import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; + import com.crossbowffs.nekosms.R; -import com.crossbowffs.nekosms.data.*; +import com.crossbowffs.nekosms.data.SmsFilterAction; +import com.crossbowffs.nekosms.data.SmsFilterData; +import com.crossbowffs.nekosms.data.SmsFilterField; +import com.crossbowffs.nekosms.data.SmsFilterMode; +import com.crossbowffs.nekosms.data.SmsFilterPatternData; import com.crossbowffs.nekosms.loader.FilterRuleLoader; -import com.crossbowffs.nekosms.widget.FragmentPagerAdapter; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; public class FilterEditorActivity extends AppCompatActivity { - private class FilterEditorPageAdapter extends FragmentPagerAdapter { - public FilterEditorPageAdapter() { - super(getFragmentManager()); - } - - @Override - public Fragment getItem(int position) { - FilterEditorFragment fragment = new FilterEditorFragment(); - Bundle args = new Bundle(1); - SmsFilterField field; - if (position == 0) { - field = SmsFilterField.SENDER; - } else if (position == 1) { - field = SmsFilterField.BODY; - } else { - throw new AssertionError("Invalid adapter position: " + position); - } - args.putSerializable(FilterEditorFragment.EXTRA_FIELD, field); - fragment.setArguments(args); - return fragment; - } - - @Override - public int getCount() { - return 2; - } - - @Override - public CharSequence getPageTitle(int position) { - if (position == 0) { - return getString(R.string.filter_field_sender); - } else if (position == 1) { - return getString(R.string.filter_field_body); - } else { - throw new AssertionError("Invalid adapter position: " + position); - } - } - } - public static final String EXTRA_ACTION = "action"; - private Toolbar mToolbar; - private TabLayout mTabLayout; - private ViewPager mViewPager; + private FrameLayout mFrameLayout; private Uri mFilterUri; private SmsFilterData mFilter; @@ -75,12 +37,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_filter_editor); mToolbar = (Toolbar)findViewById(R.id.toolbar); - mTabLayout = (TabLayout)findViewById(R.id.filter_editor_tablayout); - mViewPager = (ViewPager)findViewById(R.id.filter_editor_viewpager); - - // Set up tab pages - mViewPager.setAdapter(new FilterEditorPageAdapter()); - mTabLayout.setupWithViewPager(mViewPager); + mFrameLayout = (FrameLayout)findViewById(R.id.editor_content); // Process intent for modifying existing filter if it exists mFilterUri = getIntent().getData(); @@ -92,14 +49,6 @@ public void onCreate(Bundle savedInstanceState) { mFilter.setAction(action); } - // Select a tab based on which pattern has data - // Default to the sender tab if neither has data - if (!mFilter.getSenderPattern().hasData() && mFilter.getBodyPattern().hasData()) { - mTabLayout.getTabAt(1).select(); - } else { - mTabLayout.getTabAt(0).select(); - } - // Initialize empty patterns with some reasonable default values if (!mFilter.getSenderPattern().hasData()) { mFilter.getSenderPattern() @@ -115,15 +64,23 @@ public void onCreate(Bundle savedInstanceState) { .setCaseSensitive(false); } - // Set up toolbar - if (mFilter.getAction() == SmsFilterAction.BLOCK) { - mToolbar.setTitle(R.string.save_blacklist_rule); - } else if (mFilter.getAction() == SmsFilterAction.ALLOW) { - mToolbar.setTitle(R.string.save_whitelist_rule); - } + // Set up toolbar 现在使用radioButton 来区分黑白名单 + mToolbar.setTitle(getString(R.string.filter_editor)); mToolbar.setNavigationIcon(R.drawable.ic_done_white_24dp); setSupportActionBar(mToolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + FilterEditorFragment fragment = new FilterEditorFragment(); + getFragmentManager().beginTransaction().replace(R.id.editor_content, fragment).addToBackStack(null).commit(); + } + + private SmsFilterAction getAction() { + Intent intent = getIntent(); + String actionStr = intent.getStringExtra(EXTRA_ACTION); + if (actionStr == null) { + return SmsFilterAction.BLOCK; + } + return SmsFilterAction.parse(actionStr); } @Override @@ -156,6 +113,10 @@ public SmsFilterPatternData getPatternData(SmsFilterField field) { return mFilter.getPatternForField(field); } + public SmsFilterData getSmsFilterData() { + return mFilter; + } + private String validatePatternString(SmsFilterPatternData patternData, int fieldNameId) { if (patternData.getMode() != SmsFilterMode.REGEX) { return null; @@ -175,20 +136,11 @@ private String validatePatternString(SmsFilterPatternData patternData, int field return null; } - private SmsFilterAction getAction() { - Intent intent = getIntent(); - String actionStr = intent.getStringExtra(EXTRA_ACTION); - if (actionStr == null) { - return SmsFilterAction.BLOCK; - } - return SmsFilterAction.parse(actionStr); - } - private boolean shouldSaveFilter() { return mFilter.getSenderPattern().hasData() || mFilter.getBodyPattern().hasData(); } - private boolean validatePattern(SmsFilterPatternData patternData, int fieldNameId, int tabIndex) { + private boolean validatePattern(SmsFilterPatternData patternData, int fieldNameId) { if (!patternData.hasData()) { return true; } @@ -196,7 +148,6 @@ private boolean validatePattern(SmsFilterPatternData patternData, int fieldNameI if (patternError == null) { return true; } - mTabLayout.getTabAt(tabIndex).select(); showInvalidPatternDialog(patternError); return false; } @@ -206,10 +157,10 @@ private void saveIfValid() { discardAndFinish(); return; } - if (!validatePattern(mFilter.getSenderPattern(), R.string.invalid_pattern_field_sender, 0)) { + if (!validatePattern(mFilter.getSenderPattern(), R.string.invalid_pattern_field_sender)) { return; } - if (!validatePattern(mFilter.getBodyPattern(), R.string.invalid_pattern_field_body, 1)) { + if (!validatePattern(mFilter.getBodyPattern(), R.string.invalid_pattern_field_body)) { return; } saveAndFinish(); diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorFragment.java b/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorFragment.java index 08a793b..52451c0 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorFragment.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/FilterEditorFragment.java @@ -3,23 +3,27 @@ import android.app.Fragment; import android.content.res.Resources; import android.os.Bundle; -import androidx.annotation.Nullable; -import com.google.android.material.textfield.TextInputLayout; import android.text.Editable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.Spinner; + +import androidx.annotation.Nullable; + import com.crossbowffs.nekosms.R; -import com.crossbowffs.nekosms.data.SmsFilterField; +import com.crossbowffs.nekosms.data.SmsFilterAction; import com.crossbowffs.nekosms.data.SmsFilterMode; import com.crossbowffs.nekosms.data.SmsFilterPatternData; import com.crossbowffs.nekosms.utils.MapUtils; import com.crossbowffs.nekosms.widget.EnumAdapter; import com.crossbowffs.nekosms.widget.OnItemSelectedListenerAdapter; import com.crossbowffs.nekosms.widget.TextWatcherAdapter; +import com.google.android.material.textfield.TextInputLayout; import java.util.HashMap; import java.util.Map; @@ -38,16 +42,24 @@ public boolean toBoolean() { } } - public static final String EXTRA_FIELD = "field"; - - private SmsFilterField mField; private TextInputLayout mPatternTextInputLayout; private EditText mPatternEditText; private Spinner mModeSpinner; private Spinner mCaseSpinner; + private TextInputLayout mPatternTextInputLayout2; + private EditText mPatternEditText2; + private Spinner mModeSpinner2; + private Spinner mCaseSpinner2; + + private RadioButton rdBtnBlackList; + private RadioButton rdBtnWhiteList; + private RadioGroup rdGroup; + private EnumAdapter mModeAdapter; private EnumAdapter mCaseAdapter; + private SmsFilterPatternData mPatternData; + private SmsFilterPatternData mPatternData2; private FilterEditorActivity getEditorActivity() { return (FilterEditorActivity)getActivity(); @@ -56,7 +68,6 @@ private FilterEditorActivity getEditorActivity() { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mField = (SmsFilterField)getArguments().getSerializable(EXTRA_FIELD); } @Override @@ -66,6 +77,15 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mPatternEditText = (EditText)view.findViewById(R.id.filter_editor_pattern_edittext); mModeSpinner = (Spinner)view.findViewById(R.id.filter_editor_mode_spinner); mCaseSpinner = (Spinner)view.findViewById(R.id.filter_editor_case_spinner); + + mPatternTextInputLayout2 = (TextInputLayout)view.findViewById(R.id.filter_editor_pattern_inputlayout2); + mPatternEditText2 = (EditText)view.findViewById(R.id.filter_editor_pattern_edittext2); + mModeSpinner2 = (Spinner)view.findViewById(R.id.filter_editor_mode_spinner2); + mCaseSpinner2 = (Spinner)view.findViewById(R.id.filter_editor_case_spinner2); + + rdGroup =view.findViewById(R.id.rd_group); + rdBtnBlackList =view.findViewById(R.id.rdBtn_blacklist); + rdBtnWhiteList =view.findViewById(R.id.rdBtn_whitelist); return view; } @@ -77,13 +97,29 @@ public void onActivityCreated(Bundle savedInstanceState) { mModeAdapter = new EnumAdapter<>(getEditorActivity(), android.R.layout.simple_spinner_dropdown_item, SmsFilterMode.class); mModeAdapter.setStringMap(getModeMap()); mModeSpinner.setAdapter(mModeAdapter); + mModeSpinner2.setAdapter(mModeAdapter); mCaseAdapter = new EnumAdapter<>(getEditorActivity(), android.R.layout.simple_spinner_dropdown_item, CaseSensitivity.class); mCaseAdapter.setStringMap(getCaseMap()); mCaseSpinner.setAdapter(mCaseAdapter); + mCaseSpinner2.setAdapter(mCaseAdapter); + + // Load SenderPattern and BodyPattern + mPatternData = getEditorActivity().getSmsFilterData().getSenderPattern(); + mPatternData2 = getEditorActivity().getSmsFilterData().getBodyPattern(); - // Load pattern data corresponding to the current tab - mPatternData = getEditorActivity().getPatternData(mField); + //默认是黑名单,如果是白名单的话,勾选上白名单的按钮 + rdBtnWhiteList.setChecked(getEditorActivity().getSmsFilterData().getAction()==SmsFilterAction.ALLOW); + rdGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + if(checkedId==R.id.rdBtn_whitelist){ + getEditorActivity().getSmsFilterData().setAction(SmsFilterAction.ALLOW); + }else { + getEditorActivity().getSmsFilterData().setAction(SmsFilterAction.BLOCK); + } + } + }); // Disable hint animation as workaround for drawing issue during activity creation // See https://code.google.com/p/android/issues/detail?id=179776 @@ -112,6 +148,42 @@ public void onItemSelected(AdapterView parent, View view, int position, long mPatternData.setCaseSensitive(mCaseAdapter.getItem(position).toBoolean()); } }); + + // BODY—————————— + mPatternTextInputLayout2.setHintAnimationEnabled(false); + mPatternEditText2.setText(mPatternData2.getPattern()); + mPatternTextInputLayout2.setHintAnimationEnabled(true); + mPatternEditText2.addTextChangedListener(new TextWatcherAdapter() { + @Override + public void afterTextChanged(Editable s) { + mPatternData2.setPattern(s.toString()); + } + }); + + mModeSpinner2.setSelection(mModeAdapter.getPosition(mPatternData2.getMode())); + mModeSpinner2.setOnItemSelectedListener(new OnItemSelectedListenerAdapter() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mPatternData2.setMode(mModeAdapter.getItem(position)); + } + }); + + mCaseSpinner2.setSelection(mCaseAdapter.getPosition(CaseSensitivity.fromBoolean(mPatternData2.isCaseSensitive()))); + mCaseSpinner2.setOnItemSelectedListener(new OnItemSelectedListenerAdapter() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mPatternData2.setCaseSensitive(mCaseAdapter.getItem(position).toBoolean()); + } + }); + + // 移动光标到合适的地方 + if (!mPatternData.hasData() && mPatternData2.hasData()) { + mPatternEditText2.setSelection(mPatternEditText2.getText().length()); + mPatternEditText2.requestFocus(); + } else { + mPatternEditText.setSelection(mPatternEditText.getText().length()); + mPatternEditText.requestFocus(); + } } private Map getModeMap() { diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesAdapter.java b/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesAdapter.java index 5199f16..3725229 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesAdapter.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesAdapter.java @@ -1,18 +1,29 @@ package com.crossbowffs.nekosms.app; import android.database.Cursor; -import androidx.recyclerview.widget.RecyclerView; +import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + import com.crossbowffs.nekosms.R; -import com.crossbowffs.nekosms.data.*; +import com.crossbowffs.nekosms.data.SmsFilterAction; +import com.crossbowffs.nekosms.data.SmsFilterData; +import com.crossbowffs.nekosms.data.SmsFilterField; +import com.crossbowffs.nekosms.data.SmsFilterMode; +import com.crossbowffs.nekosms.data.SmsFilterPatternData; import com.crossbowffs.nekosms.loader.FilterRuleLoader; import com.crossbowffs.nekosms.widget.RecyclerCursorAdapter; -/* package */ class FilterRulesAdapter extends RecyclerCursorAdapter { +import java.util.List; + +/* package */ public class FilterRulesAdapter extends RecyclerCursorAdapter { + public static class UserFiltersItemHolder extends RecyclerView.ViewHolder { + public final TextView mActionInfoTextView; public final TextView mSenderInfoTextView; public final TextView mSenderPatternTextView; public final TextView mBodyInfoTextView; @@ -21,6 +32,7 @@ public static class UserFiltersItemHolder extends RecyclerView.ViewHolder { public UserFiltersItemHolder(View itemView) { super(itemView); + mActionInfoTextView = (TextView)itemView.findViewById(R.id.filter_rule_action_info_textview); mSenderInfoTextView = (TextView)itemView.findViewById(R.id.filter_rule_sender_info_textview); mSenderPatternTextView = (TextView)itemView.findViewById(R.id.filter_rule_sender_pattern_textview); mBodyInfoTextView = (TextView)itemView.findViewById(R.id.filter_rule_body_info_textview); @@ -29,6 +41,7 @@ public UserFiltersItemHolder(View itemView) { } private final FilterRulesFragment mFragment; + private List mSmsFilterDatas; public FilterRulesAdapter(FilterRulesFragment fragment) { mFragment = fragment; @@ -49,10 +62,15 @@ protected int[] onBindColumns(Cursor cursor) { @Override public void onBindViewHolder(UserFiltersItemHolder holder, Cursor cursor) { SmsFilterData filterData = FilterRuleLoader.get().getData(cursor, getColumns(), holder.mFilterData); + holder.mFilterData = filterData; final long id = filterData.getId(); + + SmsFilterAction action=filterData.getAction(); SmsFilterPatternData senderPattern = filterData.getSenderPattern(); SmsFilterPatternData bodyPattern = filterData.getBodyPattern(); + + bindTextViews(action, holder.mActionInfoTextView); bindTextViews(senderPattern, holder.mSenderInfoTextView, holder.mSenderPatternTextView); bindTextViews(bodyPattern, holder.mBodyInfoTextView, holder.mBodyPatternTextView); holder.itemView.setOnClickListener(new View.OnClickListener() { @@ -63,6 +81,18 @@ public void onClick(View v) { }); } + private void bindTextViews(SmsFilterAction action, TextView infoView) { + infoView.setText(action.name()); + infoView.setVisibility(View.VISIBLE); + if(action==SmsFilterAction.ALLOW){ + infoView.setTextColor(Color.GREEN); + infoView.setAlpha(0.5f); + }else { + infoView.setTextColor(Color.RED); + infoView.setAlpha(0.5f); + } + } + private void bindTextViews(SmsFilterPatternData pattern, TextView infoView, TextView patternView) { if (pattern.hasData()) { infoView.setText(buildFilterInfoString(R.string.format_filter_info, pattern)); diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesFragment.java b/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesFragment.java index 3157d05..af1e1f9 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesFragment.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/FilterRulesFragment.java @@ -1,5 +1,6 @@ package com.crossbowffs.nekosms.app; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.LoaderManager; import android.content.ContentUris; @@ -10,9 +11,6 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.LinearLayoutManager; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; @@ -21,11 +19,15 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.LinearLayoutManager; + import com.crossbowffs.nekosms.R; import com.crossbowffs.nekosms.backup.BackupLoader; import com.crossbowffs.nekosms.backup.ExportResult; import com.crossbowffs.nekosms.backup.ImportResult; -import com.crossbowffs.nekosms.data.SmsFilterAction; import com.crossbowffs.nekosms.data.SmsFilterData; import com.crossbowffs.nekosms.loader.FilterRuleLoader; import com.crossbowffs.nekosms.provider.DatabaseContract; @@ -33,7 +35,9 @@ import com.crossbowffs.nekosms.widget.DialogAsyncTask; import com.crossbowffs.nekosms.widget.ListRecyclerView; -public class FilterRulesFragment extends MainFragment implements LoaderManager.LoaderCallbacks { +public class FilterRulesFragment + extends MainFragment + implements LoaderManager.LoaderCallbacks { private static final int IMPORT_BACKUP_REQUEST = 1853; private static final int EXPORT_BACKUP_REQUEST = 1854; public static final String EXTRA_ACTION = "action"; @@ -42,13 +46,11 @@ public class FilterRulesFragment extends MainFragment implements LoaderManager.L private ListRecyclerView mRecyclerView; private TextView mEmptyView; private FilterRulesAdapter mAdapter; - private SmsFilterAction mAction; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - mAction = SmsFilterAction.parse(getArguments().getString(EXTRA_ACTION)); } @Override @@ -78,20 +80,13 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { @Override public void onClick(View v) { Intent intent = new Intent(getContext(), FilterEditorActivity.class); - intent.putExtra(FilterEditorActivity.EXTRA_ACTION, mAction.name()); + //intent.putExtra(FilterEditorActivity.EXTRA_ACTION, null); startActivity(intent); } }); - - // Set strings according to which section we're displaying - if (mAction == SmsFilterAction.BLOCK) { - setTitle(R.string.blacklist_rules); - mEmptyView.setText(R.string.blacklist_rules_empty_text); - } else if (mAction == SmsFilterAction.ALLOW) { - setTitle(R.string.whitelist_rules); - mEmptyView.setText(R.string.whitelist_rules_empty_text); - } - + + setTitle(R.string.list_rules); + mEmptyView.setText(R.string.list_rules_empty_text); // Handle import requests as necessary onNewArguments(getArguments()); } @@ -114,20 +109,35 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMen MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.context_filter_rules, menu); menu.setHeaderTitle(R.string.filter_actions); + + int clickPosition = ((ListRecyclerView.ContextMenuInfo)menuInfo).mPosition; + if(clickPosition==0){ + menu.findItem(R.id.menu_item_up).setEnabled(false); + }else if(clickPosition==mAdapter.getItemCount()-1){ + menu.findItem(R.id.menu_item_down).setEnabled(false); + } + } @Override public boolean onContextItemSelected(MenuItem item) { - ListRecyclerView.ContextMenuInfo info = (ListRecyclerView.ContextMenuInfo)item.getMenuInfo(); + ListRecyclerView.ContextMenuInfo info = (ListRecyclerView.ContextMenuInfo) item.getMenuInfo(); switch (item.getItemId()) { - case R.id.menu_item_edit_filter: - startFilterEditorActivity(info.mId); - return true; - case R.id.menu_item_delete_filter: - deleteFilter(info.mId); - return true; - default: - return super.onContextItemSelected(item); + case R.id.menu_item_edit_filter: + startFilterEditorActivity(info.mId); + return true; + case R.id.menu_item_delete_filter: + deleteFilter(info.mId); + return true; + case R.id.menu_item_up: + FilterRuleLoader.get().swapId(getContext(), info.mId, mAdapter.getItemId(info.mPosition - 1)); + return true; + case R.id.menu_item_down: + FilterRuleLoader.get().swapId(getContext(), info.mId, mAdapter.getItemId(info.mPosition + 1)); + return true; + + default: + return super.onContextItemSelected(item); } } @@ -150,12 +160,12 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public Loader onCreateLoader(int id, Bundle args) { return new CursorLoader( - getContext(), - DatabaseContract.FilterRules.CONTENT_URI, - DatabaseContract.FilterRules.ALL, - DatabaseContract.FilterRules.ACTION + "=?", - new String[] {mAction.name()}, - null + getContext(), + DatabaseContract.FilterRules.CONTENT_URI, + DatabaseContract.FilterRules.ALL, + null, + null, + null ); } @@ -214,6 +224,7 @@ public void onClick(DialogInterface dialog, int which) { .show(); } + @SuppressLint("StaticFieldLeak") private void importFilterRules(final Uri uri) { new DialogAsyncTask(getContext(), R.string.progress_importing) { @Override @@ -246,6 +257,7 @@ protected void onPostExecute(ImportResult result) { }.execute(); } + @SuppressLint("StaticFieldLeak") private void exportFilterRules(final Uri uri) { new DialogAsyncTask(getContext(), R.string.progress_exporting) { @Override diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/MainActivity.java b/app/src/main/java/com/crossbowffs/nekosms/app/MainActivity.java index ad18e4c..95b5223 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/MainActivity.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/MainActivity.java @@ -9,17 +9,6 @@ import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.navigation.NavigationView; -import com.google.android.material.snackbar.Snackbar; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import android.text.Html; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -27,21 +16,37 @@ import android.view.View; import android.widget.TextView; import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.drawerlayout.widget.DrawerLayout; + import com.crossbowffs.nekosms.BuildConfig; import com.crossbowffs.nekosms.R; import com.crossbowffs.nekosms.consts.PreferenceConsts; -import com.crossbowffs.nekosms.data.SmsFilterAction; import com.crossbowffs.nekosms.provider.DatabaseContract; import com.crossbowffs.nekosms.utils.IOUtils; import com.crossbowffs.nekosms.utils.Xlog; import com.crossbowffs.nekosms.utils.XposedUtils; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.navigation.NavigationView; +import com.google.android.material.snackbar.Snackbar; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { public static final String EXTRA_SECTION = "section"; - public static final String EXTRA_SECTION_BLACKLIST_RULES = "blacklist_rules"; - public static final String EXTRA_SECTION_WHITELIST_RULES = "whitelist_rules"; + public static final String EXTRA_SECTION_LIST_RULES = "list_rules"; public static final String EXTRA_SECTION_BLOCKED_MESSAGES = "blocked_messages"; public static final String EXTRA_SECTION_SETTINGS = "settings"; @@ -65,6 +70,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private ActionBarDrawerToggle mDrawerToggle; private Set mSnackbars; private Fragment mContentFragment; + private String mContentSection; private SharedPreferences mInternalPrefs; @@ -128,7 +134,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } // Set the section that was selected previously - String section = mInternalPrefs.getString(PreferenceConsts.KEY_SELECTED_SECTION, EXTRA_SECTION_BLACKLIST_RULES); + String section = mInternalPrefs.getString(PreferenceConsts.KEY_SELECTED_SECTION, EXTRA_SECTION_LIST_RULES); setContentSection(section); } } @@ -153,11 +159,8 @@ public void onConfigurationChanged(Configuration newConfig) { public boolean onNavigationItemSelected(@NonNull MenuItem item) { mDrawerLayout.closeDrawer(mNavigationView); switch (item.getItemId()) { - case R.id.main_drawer_blacklist_rules: - setContentSection(EXTRA_SECTION_BLACKLIST_RULES); - return true; - case R.id.main_drawer_whitelist_rules: - setContentSection(EXTRA_SECTION_WHITELIST_RULES); + case R.id.main_drawer_list_rules: + setContentSection(EXTRA_SECTION_LIST_RULES); return true; case R.id.main_drawer_blocked_messages: setContentSection(EXTRA_SECTION_BLOCKED_MESSAGES); @@ -209,7 +212,7 @@ private boolean handleIntent(Intent intent) { Xlog.i("Got ACTION_VIEW intent with (maybe) backup file URI"); Bundle args = new Bundle(1); args.putParcelable(FilterRulesFragment.ARG_IMPORT_URI, uri); - setContentSection(EXTRA_SECTION_BLACKLIST_RULES, args); + setContentSection(EXTRA_SECTION_LIST_RULES, args); } // Kind of a hacky workaround; this ensures that we only execute the @@ -246,21 +249,13 @@ private boolean setContentSection(String key, Bundle args) { Fragment fragment; int navId; switch (key) { - case EXTRA_SECTION_BLACKLIST_RULES: + case EXTRA_SECTION_LIST_RULES: fragment = new FilterRulesFragment(); if (args == null) { args = new Bundle(); } - args.putString(FilterRulesFragment.EXTRA_ACTION, SmsFilterAction.BLOCK.name()); - navId = R.id.main_drawer_blacklist_rules; - break; - case EXTRA_SECTION_WHITELIST_RULES: - fragment = new FilterRulesFragment(); - if (args == null) { - args = new Bundle(); - } - args.putString(FilterRulesFragment.EXTRA_ACTION, SmsFilterAction.ALLOW.name()); - navId = R.id.main_drawer_whitelist_rules; + args.putString(FilterRulesFragment.EXTRA_ACTION, null); + navId = R.id.main_drawer_list_rules; break; case EXTRA_SECTION_BLOCKED_MESSAGES: fragment = new BlockedMessagesFragment(); @@ -272,7 +267,7 @@ private boolean setContentSection(String key, Bundle args) { break; default: Xlog.e("Unknown context section: %s", key); - return setContentSection(EXTRA_SECTION_BLACKLIST_RULES); + return setContentSection(EXTRA_SECTION_LIST_RULES); } if (args != null) { @@ -311,6 +306,12 @@ public Snackbar makeSnackbar(int textId) { return snackbar; } + public Snackbar makeSnackbar(CharSequence text) { + Snackbar snackbar = Snackbar.make(mCoordinatorLayout, text, Snackbar.LENGTH_LONG); + mSnackbars.add(snackbar); + return snackbar; + } + public void dismissSnackbar() { // Items will be automatically removed from the cache // once the references are GC'd @@ -442,7 +443,7 @@ private void showAboutDialog() { GITHUB_URL, WIKI_URL)); AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name) + ' ' + VERSION_NAME) + .setTitle(getString(R.string.app_name) + ' ' + VERSION_NAME ) .setMessage(html) .setPositiveButton(R.string.close, null) .show(); diff --git a/app/src/main/java/com/crossbowffs/nekosms/app/MainFragment.java b/app/src/main/java/com/crossbowffs/nekosms/app/MainFragment.java index db54f21..b0fe9e0 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/app/MainFragment.java +++ b/app/src/main/java/com/crossbowffs/nekosms/app/MainFragment.java @@ -4,9 +4,10 @@ import android.content.Context; import android.os.Build; import android.os.Bundle; -import com.google.android.material.snackbar.Snackbar; import android.view.View; +import com.google.android.material.snackbar.Snackbar; + public class MainFragment extends Fragment { public MainActivity getMainActivity() { return (MainActivity)getActivity(); @@ -25,6 +26,10 @@ private Snackbar makeSnackbar(int textId) { return getMainActivity().makeSnackbar(textId); } + private Snackbar makeSnackbar(CharSequence text) { + return getMainActivity().makeSnackbar(text); + } + private Snackbar makeSnackbar(int textId, int actionTextId, View.OnClickListener listener) { return makeSnackbar(textId).setAction(actionTextId, listener); } @@ -37,10 +42,18 @@ public void showSnackbar(int textId) { makeSnackbar(textId).show(); } + public void showSnackbar(CharSequence text) { + makeSnackbar(text).show(); + } + public void setTitle(int titleId) { getMainActivity().setTitle(titleId); } + public void setTitle(int titleId, Object... formatArgs) { + getMainActivity().setTitle(getString(titleId,formatArgs)); + } + public void enableFab(int iconId, View.OnClickListener listener) { getMainActivity().enableFab(iconId, listener); } diff --git a/app/src/main/java/com/crossbowffs/nekosms/consts/PreferenceConsts.java b/app/src/main/java/com/crossbowffs/nekosms/consts/PreferenceConsts.java index caae62c..fc6d1f0 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/consts/PreferenceConsts.java +++ b/app/src/main/java/com/crossbowffs/nekosms/consts/PreferenceConsts.java @@ -10,6 +10,8 @@ public final class PreferenceConsts { public static final String KEY_ENABLE = "pref_enable"; public static final boolean KEY_ENABLE_DEFAULT = true; + public static final String KEY_PRIORITY_ENABLE = "pref_priority_enable"; + public static final boolean KEY_PRIORITY_DEFAULT = false; public static final String KEY_WHITELIST_CONTACTS = "pref_whitelist_contacts"; public static final boolean KEY_WHITELIST_CONTACTS_DEFAULT = false; public static final String KEY_NOTIFICATIONS_ENABLE = "pref_notifications_enable"; diff --git a/app/src/main/java/com/crossbowffs/nekosms/filters/SmsFilterLoader.java b/app/src/main/java/com/crossbowffs/nekosms/filters/SmsFilterLoader.java index 92abff8..1624118 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/filters/SmsFilterLoader.java +++ b/app/src/main/java/com/crossbowffs/nekosms/filters/SmsFilterLoader.java @@ -1,16 +1,23 @@ package com.crossbowffs.nekosms.filters; -import android.content.*; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; + import com.crossbowffs.nekosms.BuildConfig; +import com.crossbowffs.nekosms.consts.PreferenceConsts; import com.crossbowffs.nekosms.data.SmsFilterAction; import com.crossbowffs.nekosms.data.SmsFilterData; import com.crossbowffs.nekosms.loader.FilterRuleLoader; import com.crossbowffs.nekosms.provider.DatabaseContract; import com.crossbowffs.nekosms.utils.Xlog; import com.crossbowffs.nekosms.widget.CursorWrapper; +import com.crossbowffs.remotepreferences.RemotePreferences; import java.util.ArrayList; import java.util.List; @@ -61,11 +68,16 @@ public boolean shouldBlockMessage(String sender, String body) { return false; } - private List getFilters() { + private List getFilters() {//boolean priority_enable List filters = mCachedFilters; if (filters == null) { Xlog.i("Cached SMS filters dirty, loading from database"); - filters = mCachedFilters = loadFilters(); + RemotePreferences mPreferences = new RemotePreferences(mContext, PreferenceConsts.REMOTE_PREFS_AUTHORITY, PreferenceConsts.FILE_MAIN, true); + if (mPreferences.getBoolean(PreferenceConsts.KEY_PRIORITY_ENABLE, PreferenceConsts.KEY_PRIORITY_DEFAULT)) { + filters = mCachedFilters = loadFiltersByPriority(); + } else { + filters = mCachedFilters = loadFilters(); + } } return filters; } @@ -91,6 +103,8 @@ private List loadFilters() { // rules to go into the blacklist, but all rules will // be merged into the whitelist list in the end (with // whitelist rules coming first). + + // 白名单优先 ArrayList whitelist = new ArrayList<>(count); ArrayList blacklist = new ArrayList<>(count); @@ -120,6 +134,47 @@ private List loadFilters() { } } + private List loadFiltersByPriority() { + try (CursorWrapper filterCursor = FilterRuleLoader.get().queryAll(mContext)) { + if (filterCursor == null) { + // This might occur if the app has been uninstalled (removing the DB), + // but the user has not rebooted their device yet. We should not filter + // any messages in this state. + Xlog.e("Failed to load SMS filters (queryAll returned null)"); + return null; + } + + int count = filterCursor.getCount(); + Xlog.i("filterCursor.getCount() = %d", count); + + // It's better to just over-reserve since we expect most + // rules to go into the blacklist, but all rules will + // be merged into the whitelist list in the end (with + // whitelist rules coming first). + + // 按照_id顺序作为优先级 + ArrayList rulelist = new ArrayList<>(count); + + SmsFilterData data = new SmsFilterData(); + while (filterCursor.moveToNext()) { + SmsFilter filter; + try { + data = filterCursor.get(data); + filter = new SmsFilter(data); + } catch (Exception e) { + Xlog.e("Failed to load SMS filter", e); + continue; + } + + rulelist.add(filter); + } + + Xlog.i("Loaded %d rulelist filters", rulelist.size()); + rulelist.trimToSize(); + return rulelist; + } + } + private ContentObserver registerContentObserver() { Xlog.i("Registering SMS filter content observer"); diff --git a/app/src/main/java/com/crossbowffs/nekosms/loader/FilterRuleLoader.java b/app/src/main/java/com/crossbowffs/nekosms/loader/FilterRuleLoader.java index 576ad69..b5fa2d6 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/loader/FilterRuleLoader.java +++ b/app/src/main/java/com/crossbowffs/nekosms/loader/FilterRuleLoader.java @@ -1,12 +1,17 @@ package com.crossbowffs.nekosms.loader; +import static com.crossbowffs.nekosms.provider.DatabaseContract.FilterRules; + import android.content.ContentProviderOperation; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; +import android.database.DatabaseUtils; import android.net.Uri; import android.os.RemoteException; + import com.crossbowffs.nekosms.data.SmsFilterAction; import com.crossbowffs.nekosms.data.SmsFilterData; import com.crossbowffs.nekosms.data.SmsFilterMode; @@ -19,8 +24,6 @@ import java.util.ArrayList; import java.util.List; -import static com.crossbowffs.nekosms.provider.DatabaseContract.FilterRules; - public class FilterRuleLoader extends AutoContentLoader { private static FilterRuleLoader sInstance; @@ -107,6 +110,58 @@ protected ContentValues serialize(SmsFilterData data) { return values; } + public void swapId(Context context, long firstId, long secondId) { + Uri firstUri = ContentUris.withAppendedId(DatabaseContract.FilterRules.CONTENT_URI, firstId); + Uri secondUri = ContentUris.withAppendedId(DatabaseContract.FilterRules.CONTENT_URI, secondId); + + SmsFilterData firstData = query(context, firstUri); + SmsFilterData secondData = query(context, secondUri); + + firstData.setId(secondId); + secondData.setId(firstId); + + update(context, firstUri, secondData, false); + update(context, secondUri, firstData, false); + } + + public void swapId(Context context, SmsFilterData firstData, SmsFilterData secondData) { + long firstId = firstData.getId(); + long secondId = secondData.getId(); + + Uri firstUri = ContentUris.withAppendedId(DatabaseContract.FilterRules.CONTENT_URI, firstId); + Uri secondUri = ContentUris.withAppendedId(DatabaseContract.FilterRules.CONTENT_URI, secondId); + + firstData.setId(secondId); + secondData.setId(firstId); + + update(context, firstUri, secondData, false); + update(context, secondUri, firstData, false); + } + + + static String whereClause(SmsFilterData data) { + return FilterRules.ACTION + " = " + DatabaseUtils.sqlEscapeString(data.getAction().name()) + + " AND " + whereClauseForOther(FilterRules.SENDER_MODE, data.getSenderPattern().getMode()) + + " AND " + whereClauseForOther(FilterRules.SENDER_PATTERN, data.getSenderPattern().getPattern()) + + " AND " + whereClauseForOther(FilterRules.BODY_MODE, data.getBodyPattern().getMode()) + + " AND " + whereClauseForOther(FilterRules.BODY_PATTERN, data.getBodyPattern().getPattern()) + ; //whereClauseForIssuer(index.getIssuer()) + } + + private static String whereClauseForOther(String column,String value) { + if (value != null) { + return column + " = " + DatabaseUtils.sqlEscapeString(value); + } + return column + " IS NULL"; + } + + private static String whereClauseForOther(String column,Enum value) { + if (value != null) { + return column + " = " + DatabaseUtils.sqlEscapeString(value.name()); + } + return column + " IS NULL"; + } + public Uri update(Context context, Uri filterUri, SmsFilterData filterData, boolean insertIfError) { if (filterUri == null && insertIfError) { return insert(context, filterData); diff --git a/app/src/main/java/com/crossbowffs/nekosms/provider/DatabaseHelper.java b/app/src/main/java/com/crossbowffs/nekosms/provider/DatabaseHelper.java index be75610..9382c65 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/provider/DatabaseHelper.java +++ b/app/src/main/java/com/crossbowffs/nekosms/provider/DatabaseHelper.java @@ -1,19 +1,20 @@ package com.crossbowffs.nekosms.provider; +import static com.crossbowffs.nekosms.provider.DatabaseContract.BlockedMessages; +import static com.crossbowffs.nekosms.provider.DatabaseContract.FilterRules; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; + import com.crossbowffs.nekosms.BuildConfig; import com.crossbowffs.nekosms.data.SmsFilterAction; import com.crossbowffs.nekosms.data.SmsFilterField; import com.crossbowffs.nekosms.utils.MapUtils; import com.crossbowffs.nekosms.utils.Xlog; -import static com.crossbowffs.nekosms.provider.DatabaseContract.BlockedMessages; -import static com.crossbowffs.nekosms.provider.DatabaseContract.FilterRules; - /* package */ class DatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "nekosms.db"; private static final int DATABASE_VERSION = BuildConfig.DATABASE_VERSION; @@ -130,7 +131,7 @@ private void upgrade8To11(SQLiteDatabase db) { values.put(BlockedMessages.TIME_RECEIVED, messagesCursor.getLong(3)); values.put(BlockedMessages.READ, 1); values.put(BlockedMessages.SEEN, 1); - values.put(BlockedMessages.SUB_ID, 0); + values.put(BlockedMessages.SUB_ID, 1); db.insert(BlockedMessages.TABLE, null, values); } messagesCursor.close(); diff --git a/app/src/main/java/com/crossbowffs/nekosms/widget/AutoContentLoader.java b/app/src/main/java/com/crossbowffs/nekosms/widget/AutoContentLoader.java index be4816f..00ace48 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/widget/AutoContentLoader.java +++ b/app/src/main/java/com/crossbowffs/nekosms/widget/AutoContentLoader.java @@ -118,6 +118,16 @@ public boolean delete(Context context, Uri uri) { return deletedRows != 0; } + public int delete(Context context,String where,String[] selectionArgs) { + ContentResolver contentResolver = context.getContentResolver(); + return contentResolver.delete(getContentUri(), where, selectionArgs); + } + + public Cursor query(Context context, String where, String[] selectionArgs,String orderBy) { + ContentResolver contentResolver = context.getContentResolver(); + return contentResolver.query(getContentUri(), mColumnNames, where, selectionArgs, orderBy); + } + protected void updateAll(Context context, ContentValues values) { updateAll(context, values, null, null); } @@ -137,6 +147,12 @@ protected boolean update(Context context, Uri uri, ContentValues values) { return updatedRows != 0; } + public int update(Context context, ContentValues values,String where,String[] selectionArgs) { + ContentResolver contentResolver = context.getContentResolver(); + int updatedRows = contentResolver.update(getContentUri(), values, where, selectionArgs); + return updatedRows; + } + protected Uri convertIdToUri(long id) { return ContentUris.withAppendedId(getContentUri(), id); } diff --git a/app/src/main/java/com/crossbowffs/nekosms/widget/RecyclerCursorAdapter.java b/app/src/main/java/com/crossbowffs/nekosms/widget/RecyclerCursorAdapter.java index ec2b736..6480026 100644 --- a/app/src/main/java/com/crossbowffs/nekosms/widget/RecyclerCursorAdapter.java +++ b/app/src/main/java/com/crossbowffs/nekosms/widget/RecyclerCursorAdapter.java @@ -2,8 +2,12 @@ import android.database.Cursor; import android.provider.BaseColumns; + import androidx.recyclerview.widget.RecyclerView; +import java.util.HashSet; +import java.util.Set; + public abstract class RecyclerCursorAdapter extends RecyclerView.Adapter { private Cursor mCursor; private int[] mColumns; @@ -32,6 +36,20 @@ public long getItemId(int position) { } } + public Set getAllItemId() { + Set ids = new HashSet(); + if (mCursor == null) { + return null; + } else { + if (mCursor.moveToFirst()) { + do { + ids.add(mCursor.getLong(mIdColumn)); + } while (mCursor.moveToNext()); + } + return ids; + } + } + @Override public void onBindViewHolder(VH vh, int i) { mCursor.moveToPosition(i); diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 0000000..dd4ead3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..ceb1a1e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_create_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_create_white_24dp.png index 730416c..694179b 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_create_white_24dp.png and b/app/src/main/res/drawable-hdpi/ic_create_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_edit_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_edit_white_24dp.png new file mode 100644 index 0000000..730416c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_restore_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_restore_white_24dp.png new file mode 100644 index 0000000..e8a5cb9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_restore_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..bbfbc96 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 0000000..4ef72ee Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..af7f828 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_create_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_create_white_24dp.png index 85cff0b..3856041 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_create_white_24dp.png and b/app/src/main/res/drawable-mdpi/ic_create_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_edit_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_edit_white_24dp.png new file mode 100644 index 0000000..85cff0b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_restore_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_restore_white_24dp.png new file mode 100644 index 0000000..76f5d24 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_restore_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..faefc59 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 0000000..832f5a3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..b7c7ffd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_create_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_create_white_24dp.png index 7f0ea51..67bb598 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_create_white_24dp.png and b/app/src/main/res/drawable-xhdpi/ic_create_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_edit_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_edit_white_24dp.png new file mode 100644 index 0000000..7f0ea51 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_restore_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_restore_white_24dp.png new file mode 100644 index 0000000..e5277f9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_restore_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..bfc3e39 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 0000000..32a6d91 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..6b717e0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png index 34ec709..0fdced8 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png and b/app/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png new file mode 100644 index 0000000..34ec709 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_restore_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_restore_white_24dp.png new file mode 100644 index 0000000..8358810 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_restore_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..abbb989 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/feature_graphic.png b/app/src/main/res/drawable-xxxhdpi/feature_graphic.png new file mode 100644 index 0000000..396f3c3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/feature_graphic.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 0000000..e27034d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..3964192 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_create_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_create_white_24dp.png index 9380370..d64c22e 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_create_white_24dp.png and b/app/src/main/res/drawable-xxxhdpi/ic_create_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_edit_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_edit_white_24dp.png new file mode 100644 index 0000000..9380370 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_restore_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_restore_white_24dp.png new file mode 100644 index 0000000..63997fd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_restore_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..dd5adfc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable/ic_add_rule_shortcut_foreground.xml b/app/src/main/res/drawable/ic_add_rule_shortcut_foreground.xml new file mode 100644 index 0000000..a2dc6af --- /dev/null +++ b/app/src/main/res/drawable/ic_add_rule_shortcut_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_rule_folder_white_24dp.xml b/app/src/main/res/drawable/ic_rule_folder_white_24dp.xml new file mode 100644 index 0000000..f3a88cb --- /dev/null +++ b/app/src/main/res/drawable/ic_rule_folder_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/line.xml b/app/src/main/res/drawable/line.xml new file mode 100644 index 0000000..182c718 --- /dev/null +++ b/app/src/main/res/drawable/line.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/line_drawable.xml b/app/src/main/res/drawable/line_drawable.xml new file mode 100644 index 0000000..f87e4c2 --- /dev/null +++ b/app/src/main/res/drawable/line_drawable.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/thumb.xml b/app/src/main/res/drawable/thumb.xml new file mode 100644 index 0000000..2599ac1 --- /dev/null +++ b/app/src/main/res/drawable/thumb.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb_drawable.xml b/app/src/main/res/drawable/thumb_drawable.xml new file mode 100644 index 0000000..4ee67d6 --- /dev/null +++ b/app/src/main/res/drawable/thumb_drawable.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_filter_editor.xml b/app/src/main/res/layout/activity_filter_editor.xml index b942a25..15a5d9b 100644 --- a/app/src/main/res/layout/activity_filter_editor.xml +++ b/app/src/main/res/layout/activity_filter_editor.xml @@ -1,23 +1,21 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> + - - + + - + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 7401d8b..ae07a14 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -42,6 +42,7 @@ android:layout_gravity="start" app:itemIconTint="?android:textColorPrimary" app:itemTextColor="?android:textColorPrimary" + app:headerLayout="@layout/nav_header_main" app:menu="@menu/main_drawer"/> diff --git a/app/src/main/res/layout/fragment_blocked_messages.xml b/app/src/main/res/layout/fragment_blocked_messages.xml index 42614bd..d659e48 100644 --- a/app/src/main/res/layout/fragment_blocked_messages.xml +++ b/app/src/main/res/layout/fragment_blocked_messages.xml @@ -1,6 +1,6 @@ - @@ -8,7 +8,12 @@ android:id="@+id/blocked_messages_recyclerview" android:layout_width="match_parent" android:layout_height="match_parent" - android:scrollbars="vertical"/> + android:scrollbars="vertical" + app:fastScrollEnabled="true" + app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable" + app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable" + app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable" + app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"/> - + + + + + + + + - + + + android:hint="@string/filter_pattern_sender" /> + + + + android:text="@string/filter_mode" /> + + android:layout_height="wrap_content" /> + + + android:text="@string/filter_case" /> + + android:layout_height="wrap_content" /> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/listitem_blocked_messages.xml b/app/src/main/res/layout/listitem_blocked_messages.xml index af480f4..82fdc4d 100644 --- a/app/src/main/res/layout/listitem_blocked_messages.xml +++ b/app/src/main/res/layout/listitem_blocked_messages.xml @@ -36,7 +36,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" - android:singleLine="true" android:textColor="?android:textColorPrimary" android:textSize="16sp"/> + diff --git a/app/src/main/res/layout/listitem_filter_rules.xml b/app/src/main/res/layout/listitem_filter_rules.xml index 3f2a02f..b656634 100644 --- a/app/src/main/res/layout/listitem_filter_rules.xml +++ b/app/src/main/res/layout/listitem_filter_rules.xml @@ -1,50 +1,63 @@ - + + + + android:textStyle="bold" /> + + android:textSize="16sp" /> + + android:textStyle="bold" /> + + android:textSize="16sp" /> + + diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml new file mode 100644 index 0000000..fd4857a --- /dev/null +++ b/app/src/main/res/layout/nav_header_main.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/actionmode_blocked_messages.xml b/app/src/main/res/menu/actionmode_blocked_messages.xml new file mode 100644 index 0000000..39dc024 --- /dev/null +++ b/app/src/main/res/menu/actionmode_blocked_messages.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/app/src/main/res/menu/context_filter_rules.xml b/app/src/main/res/menu/context_filter_rules.xml index 926e155..7e8a951 100644 --- a/app/src/main/res/menu/context_filter_rules.xml +++ b/app/src/main/res/menu/context_filter_rules.xml @@ -1,6 +1,12 @@ + + diff --git a/app/src/main/res/menu/main_drawer.xml b/app/src/main/res/menu/main_drawer.xml index 7024894..404674d 100644 --- a/app/src/main/res/menu/main_drawer.xml +++ b/app/src/main/res/menu/main_drawer.xml @@ -1,15 +1,10 @@ - + android:title="@string/list_rules"/> + + app:showAsAction="never"/> diff --git a/app/src/main/res/menu/options_debug.xml b/app/src/main/res/menu/options_debug.xml index 8df6c04..9e41ac0 100644 --- a/app/src/main/res/menu/options_debug.xml +++ b/app/src/main/res/menu/options_debug.xml @@ -6,5 +6,5 @@ android:id="@+id/menu_item_create_test" android:title="@string/create_test_message" android:icon="@drawable/ic_create_white_24dp" - app:showAsAction="ifRoom"/> + app:showAsAction="never"/> diff --git a/app/src/main/res/menu/options_filter_editor.xml b/app/src/main/res/menu/options_filter_editor.xml index 69f2b96..6e0d842 100644 --- a/app/src/main/res/menu/options_filter_editor.xml +++ b/app/src/main/res/menu/options_filter_editor.xml @@ -4,6 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + app:showAsAction="ifRoom"/> diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_add_rule_shortcut.xml b/app/src/main/res/mipmap-anydpi-v26/ic_add_rule_shortcut.xml new file mode 100644 index 0000000..379322f --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_add_rule_shortcut.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_add_rule_shortcut.png b/app/src/main/res/mipmap-hdpi/ic_add_rule_shortcut.png new file mode 100644 index 0000000..284d1d0 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_add_rule_shortcut.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_add_rule_shortcut.png b/app/src/main/res/mipmap-mdpi/ic_add_rule_shortcut.png new file mode 100644 index 0000000..1ecb64d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_add_rule_shortcut.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_add_rule_shortcut.png b/app/src/main/res/mipmap-xhdpi/ic_add_rule_shortcut.png new file mode 100644 index 0000000..2d8c67a Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_add_rule_shortcut.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_rule_shortcut.png b/app/src/main/res/mipmap-xxhdpi/ic_add_rule_shortcut.png new file mode 100644 index 0000000..2e03695 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_rule_shortcut.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_add_rule_shortcut.png b/app/src/main/res/mipmap-xxxhdpi/ic_add_rule_shortcut.png new file mode 100644 index 0000000..00d8f9d Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_add_rule_shortcut.png differ diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ac21390..71e7a12 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -14,6 +14,11 @@ Восстановить Отменить Отмена + Seen + Up + Down + Select All + Select Invert Открыть боковое меню @@ -36,8 +41,10 @@ Сохраняю данные в файл… + List rules Чёрный список Белый список + No list rules yet! Правила чёрного списка отсутсвуют Правила белого списка отсутсвуют %1$s %2$s%3$s @@ -90,6 +97,7 @@ Корзина + Blocked messages(%1$d) Нет заблокированных сообщений Модуль Xposed должен быть активирован Очистить заблокированные сообщения @@ -97,6 +105,10 @@ Все сообщения в корзине будут безвозвратно удалены! Удалить все заблокированные сообщения Сообщение удалено + %1$d Message deleted + %1$d Message selected + %1$d Message restored + Сообщение восстановлено в приложение SMS Сообщение не может быть восстановлено Не получилось загрузить сообщение @@ -106,6 +118,9 @@ Отправитель
%1$s

+ Send to
+ SIM %4$d

+ Время получения
%2$s

@@ -116,6 +131,8 @@ Значение + Sender Pattern + Body Pattern Правило Регистр Регулярное выражение @@ -135,6 +152,9 @@ Включить NekoSMS Перезагрузка не требуется Пожалуйста активируйте модуль Xposed! + Enable Priority + Match all rules in order + Match the whitelist rules preferentially Разрешить сообщения от контактов Не блокировать входящие SMS сообщения от номеров записанных в телефонной книге Расширенное логирование @@ -160,6 +180,7 @@ Заблокированные сообщения + New rule В чёрный список В белый список Корзина diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 359385e..30f5a35 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -14,6 +14,11 @@ 恢复 撤消 取消 + 已读 + 上移 + 下移 + 全选 + 反选 扩展导航抽屉 @@ -36,8 +41,10 @@ 正在导出数据… - 黑名单规则 - 白名单规则 + 名单规则 + 黑名单 + 白名单 + 没有任何名单规则! 没有任何黑名单规则! 没有任何白名单规则! %1$s%2$s%3$s @@ -90,13 +97,18 @@ 已拦截的信息 + 已拦截(%1$d) 没有任何垃圾信息! 需要启用Xposed模块 - 清空已拦截的信息 + 清空页面信息 删除所有信息? - 信息被删除后永远无法恢复! + 将删除页面上展示的所有信息,信息被删除后永远无法恢复! 已删除所有信息 信息已删除 + %1$d条信息已删除 + %1$d条信息已选择 + %1$d条信息已恢复 + 信息已恢复到收件箱 无法恢复信息 无法加载信息 @@ -106,6 +118,9 @@ 发送者
%1$s

+ 发送给
+ SIM %4$d

+ 发送时间
%2$s

@@ -116,6 +131,9 @@ 表达式 + 发送者表达式 + 文本表达式 + 匹配方式 大小写 正则表达式 @@ -135,6 +153,9 @@ 启用NekoSMS 用此开关不需要重启 请先启用Xposed模块! + 启用规则优先级 + 按顺序逐一匹配全部规则 + 优先匹配白名单规则 联系人白名单 不拦截来自联系人的信息 详细log模式 @@ -156,10 +177,11 @@ 点击此处打开通知设置 - %s(已拦截) + %s 信息拦截通知 + 新建名单规则 新建黑名单规则 新建白名单规则 已拦截的信息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a78eba1..94befee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,11 @@ Restore Undo Cancel + Seen + Up + Down + Select All + Select Invert Open navigation drawer @@ -36,8 +41,10 @@ Exporting data to storage… + List rules Blacklist rules Whitelist rules + No list rules yet! No blacklist rules yet! No whitelist rules yet! %1$s %2$s%3$s @@ -90,6 +97,7 @@ Blocked messages + Blocked messages(%1$d) No blocked messages! Xposed module must be enabled Clear blocked messages @@ -97,6 +105,10 @@ Your messages will be gone forever! (A very long time!) Deleted all blocked messages Message deleted + %1$d Message deleted + %1$d Message selected + %1$d Message restored + Message restored to inbox Message could not be restored Could not load message @@ -106,6 +118,9 @@ Sender
%1$s

+ Send to
+ SIM %4$d

+ Time sent
%2$s

@@ -116,6 +131,9 @@ Pattern + Sender Pattern + Body Pattern + Mode Case Regular expression @@ -135,6 +153,9 @@ Enable NekoSMS No reboot required Please enable the Xposed module first! + Enable Priority + Match all rules in order + Match the whitelist rules preferentially Whitelist contacts Don\'t block messages from contacts Verbose logging @@ -160,6 +181,7 @@ Blocked messages + New rule New blacklist rule New whitelist rule Blocked messages diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ac9dfe6..687797d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,13 +1,43 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 773a514..38fea91 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -7,12 +7,20 @@ android:summary="@string/pref_enable_summary" android:defaultValue="true" android:widgetLayout="@layout/switch_compat"/> + + + android:icon="@mipmap/ic_add_rule_shortcut" + android:shortcutShortLabel="@string/shortcut_new_rulelist_rule"> - - - - - - + +