Hi there!
I’m continuing my series of posts about creating Android application for resending notifications. Previous parts you can find here: http://dbondarchuk.com/tag/notification-resender/
So, we’ve already created our main activity, helper for working with database and now, we need to create an activity for adding/editing setting.
Let’s start! 🙂
We need to create a new Empty Activity. Let’s call it EditActivity.
First of all, we will create a layout for our activity.
content_edit.xml file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:paddingRight="20dp" android:paddingLeft="20dp" android:paddingTop="20dp" android:layout_height="match_parent"> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" android:layout_marginBottom="20dp" android:text="@string/add_setting_activity_title" android:id="@+id/addSettingActivityTitle" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:src="@android:drawable/ic_menu_help" android:id="@+id/showInfoButton" android:contentDescription="Info" /> </LinearLayout> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/name_title" android:id="@+id/textView2" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:id="@+id/nameEditText" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/title_title" android:id="@+id/textView3" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:id="@+id/titleEditText" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/body_title" android:id="@+id/textView4" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:id="@+id/bodyEditText" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/exclude_title" android:id="@+id/textView5" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:id="@+id/excludeEditText" /> <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/isExcludeRegex_title" android:layout_marginBottom="10dp" android:textAppearance="?android:attr/textAppearanceMedium" android:id="@+id/excludeRegexCheckBox" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/remove_delay_title" android:id="@+id/textView6" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="number" android:ems="10" android:text="0" android:id="@+id/removalDelayText" android:layout_marginBottom="10dp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/apps_title" android:id="@+id/appsCountTitleText" /> <TextView android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="0" android:id="@+id/appsCountText" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/selectAppsButton" android:text="@string/select_apps_title"/> </LinearLayout> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/add_setting_button" android:id="@+id/addSettingButton" android:onClick="onAddButtonClick"/> </LinearLayout> </ScrollView> </LinearLayout> |
This will create a form that will contain inputs for needed field. It will look like this:
As you can see, we wrapped our content in ScrollView – it’s needed for enabling scrolling through the screen. Another strange thing is that this ScrollView contains LinearLayout instead of direct elements. This is caused by that fact, that ScrollView can have only one child element.
Let’s look on Java code for this activity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit); Intent intent = getIntent(); if (intent.hasExtra("setting")){ Object setting = intent.getSerializableExtra("setting"); if (setting instanceof ResendSetting){ isEdit = true; originalSetting = (ResendSetting)setting; initializeEdit(originalSetting); } } Button addButton = (Button) this.findViewById(R.id.selectAppsButton); addButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent appSelectIntent = new Intent(EditActivity.this, AppSelectActivity.class); appSelectIntent.putExtra("apps", packages); startActivityForResult(appSelectIntent, 1); } }); ImageView showInfoButton = (ImageView) this.findViewById(R.id.showInfoButton); showInfoButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { new AlertDialog.Builder(EditActivity.this) .setTitle(getString(R.string.edit_info_title)) .setMessage(getString(R.string.edit_info_message)) .setIcon(android.R.drawable.ic_menu_help) .setPositiveButton(android.R.string.ok, null).show(); } }); } |
Here we will initialize our activity. The simplest thing is to show help, when we click on help button. For this purpose let’s use simple alert dialog, using string resources:
1 2 3 4 5 6 7 8 9 10 11 |
ImageView showInfoButton = (ImageView) this.findViewById(R.id.showInfoButton); showInfoButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { new AlertDialog.Builder(EditActivity.this) .setTitle(getString(R.string.edit_info_title)) .setMessage(getString(R.string.edit_info_message)) .setIcon(android.R.drawable.ic_menu_help) .setPositiveButton(android.R.string.ok, null).show(); } }); |
Okay, now we have help, that shows how fields should be filled. Interesting moment, if you want to make line break in string resource, you can achieve that like this:
<string name="edit_info_message" formatted="false">You can use such macroses in title and body:\n%appName% - will set name of application that sent notification\n%title% - title of the notification\n%text% - text of the notification</string>
- Use disabled formatting
- Use ‘\n‘ for line break
Then, we should initialize activity, if we know, that we are editing existing setting instead of creating a new one:
1 2 3 4 5 6 7 8 9 |
Intent intent = getIntent(); if (intent.hasExtra("setting")){ Object setting = intent.getSerializableExtra("setting"); if (setting instanceof ResendSetting){ isEdit = true; originalSetting = (ResendSetting)setting; initializeEdit(originalSetting); } } |
Here we are waiting that Intent (https://developer.android.com/reference/android/content/Intent.html) has extra with name “setting” that is instance of ResendSetting, if so, set isEdit flag to true and initialize fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
private void initializeEdit(ResendSetting setting){ EditText nameEdit = (EditText)this.findViewById(R.id.nameEditText); EditText titleEdit = (EditText)this.findViewById(R.id.titleEditText); EditText bodyEdit = (EditText)this.findViewById(R.id.bodyEditText); EditText excludeEdit = (EditText)this.findViewById(R.id.excludeEditText); CheckBox excludeRegexCheckbox = (CheckBox)this.findViewById(R.id.excludeRegexCheckBox); EditText removalDelayEdit = (EditText)this.findViewById(R.id.removalDelayText); TextView activityTitle = (TextView)this.findViewById(R.id.addSettingActivityTitle); TextView appsCountTextView = (TextView)this.findViewById(R.id.appsCountText); Button addButton = (Button) this.findViewById(R.id.addSettingButton); nameEdit.setText(setting.name); titleEdit.setText(setting.title); bodyEdit.setText(setting.body); excludeEdit.setText(setting.excludeRegex); excludeRegexCheckbox.setChecked(setting.useRegexForExclude); removalDelayEdit.setText(String.valueOf(setting.removeDelay)); packages = setting.apps; appsCountTextView.setText(String.valueOf(packages.length() > 0 ? packages.split("\\|").length : 0)); activityTitle.setText(getString(R.string.edit_setting_activity_title)); addButton.setText(getString(R.string.edit_setting_button)); } |
For selected applications we just show number of them. But we need to create some method to select them. We will create another Activity with list of all installed applications (for correct identifying application in notification receiver we will identify applications by their package name).
So, at first we need a class that will represent application in ListView, as we done this for list of settings.
AppSelect.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class AppSelect { public String name; public String packageName; public boolean selected; public Drawable icon; public AppSelect(String packageName, String name, Drawable icon, boolean selected){ this.name = name; this.packageName = packageName; this.icon = icon; this.selected = selected; } } |
An object that will represent application should have an icon, display name, package name and boolean value that shows if application is selected.
No we will add layout for ListView item (selectapp_listview_item.xml):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:weightSum="1" android:paddingBottom="5dp"> <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="" android:id="@+id/appSelectCheckbox"/> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" android:id="@+id/appSelectIcon"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="AppName" android:id="@+id/appSelectName" android:paddingBottom="5dp" /> </LinearLayout> |
Just a checkbox, icon and display name. Also we need an adapter (AppSelectAdapter.java):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class AppSelectArrayAdapter extends ArrayAdapter<AppSelect> { private Context context; private int resource; private ArrayList<AppSelect> objects; public AppSelectArrayAdapter(Context context, int resource, ArrayList<AppSelect> objects) { super(context, resource, objects); this.context=context; this.resource=resource; this.objects=objects; } @Override public View getView(final int position, final View convertView, final ViewGroup parent) { LayoutInflater inflater=((Activity) context).getLayoutInflater(); final View row=inflater.inflate(resource,parent,false); final TextView nameTextView = (TextView)row.findViewById(R.id.appSelectName); final CheckBox selectedCheckbox = (CheckBox) row.findViewById(R.id.appSelectCheckbox); final ImageView iconImage = (ImageView) row.findViewById(R.id.appSelectIcon); selectedCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { objects.get(position).selected = checked; } }); nameTextView.setText(objects.get(position).name); selectedCheckbox.setChecked(objects.get(position).selected); iconImage.setImageDrawable(objects.get(position).icon); return row; } } |
Here we setting selected property on checkbox click
Now we need an empty activity for that (AppSelectActivity):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="ua.com.revi.notificationresender.AppSelectActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" android:text="@string/select_apps_activity_title" android:id="@+id/selectAppsTextView" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> <ListView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/appsSelectListView" android:layout_alignParentStart="@+id/selectAppsTextView" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:choiceMode="none" android:clickable="true" android:contextClickable="false" android:dividerHeight="0dp" android:divider="@null" android:touchscreenBlocksFocus="false" android:paddingLeft="5dp" android:paddingTop="5dp" android:paddingRight="5dp" android:paddingBottom="5dp" android:layout_below="@+id/selectAppsTextView" /> </RelativeLayout> |
And Java code for that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
public class AppSelectActivity extends AppCompatActivity { private ArrayList<AppSelect> appSelects = new ArrayList<AppSelect>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_app_select); Intent intent = getIntent(); String appsExtra = intent.getStringExtra("apps"); List<String> packages = Arrays.asList(appsExtra.split("\\|")); PackageManager pm = getPackageManager(); List<PackageInfo> apps = pm.getInstalledPackages(0); for (int i = 0; i < apps.size(); i++){ PackageInfo packageInfo = apps.get(i); AppSelect appSelect = new AppSelect( packageInfo.packageName, packageInfo.applicationInfo.loadLabel(pm).toString(), packageInfo.applicationInfo.loadIcon(pm), packages.contains(packageInfo.packageName)); appSelects.add(appSelect); } AppSelectArrayAdapter adapter = new AppSelectArrayAdapter(this, R.layout.selectapp_listview_item, appSelects); ListView listView=(ListView) findViewById(R.id.appsSelectListView); listView.setAdapter(adapter); } @Override public void onBackPressed() { ArrayList<AppSelect> selected = new ArrayList<>(); for (int i = 0; i < appSelects.size(); i++){ if (appSelects.get(i).selected){ selected.add(appSelects.get(i)); } } String apps = ""; for (int i = 0; i < selected.size(); i++){ apps += selected.get(i).packageName; if (i != selected.size() - 1){ apps += "|"; } } Intent intent = new Intent(); intent.putExtra("apps", apps); setResult(RESULT_OK, intent); finish(); } } |
Here, on activity create, we getting already selected package names from the intent extra, splitting them by ‘|‘ character (Note: as you can see, split method takes “\\|” as parameter, instead of simple “|“. It’s needed due to that fact, that split accept a regex pattern, so we need to escape “|” sign by back-slash, and then escape back-slash using back-slash for Java string)
The next step is to get all installed applications. PackageManager allows us to do that:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
PackageManager pm = getPackageManager(); List<PackageInfo> apps = pm.getInstalledPackages(0); for (int i = 0; i < apps.size(); i++){ PackageInfo packageInfo = apps.get(i); AppSelect appSelect = new AppSelect( packageInfo.packageName, packageInfo.applicationInfo.loadLabel(pm).toString(), packageInfo.applicationInfo.loadIcon(pm), packages.contains(packageInfo.packageName)); appSelects.add(appSelect); } |
We are getting all installed applications and then converting them into our AppSelect objects. And finally we can push them into the ListView:
1 2 3 |
AppSelectArrayAdapter adapter = new AppSelectArrayAdapter(this, R.layout.selectapp_listview_item, appSelects); ListView listView=(ListView) findViewById(R.id.appsSelectListView); listView.setAdapter(adapter); |
Also we need to apply our selection somehow. For simplify solution, let’s apply it on pressing back button:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public void onBackPressed() { ArrayList<AppSelect> selected = new ArrayList<>(); for (int i = 0; i < appSelects.size(); i++){ if (appSelects.get(i).selected){ selected.add(appSelects.get(i)); } } String apps = ""; for (int i = 0; i < selected.size(); i++){ apps += selected.get(i).packageName; if (i != selected.size() - 1){ apps += "|"; } } Intent intent = new Intent(); intent.putExtra("apps", apps); setResult(RESULT_OK, intent); finish(); } |
The logic is simple: get selected applications, join their package names using “|” character, put this string into the Intent, set result (OK) with this intent.
Note: finish() is needed for closing current activity
Okay, so we created an activity for selecting apps, that will look like this:
Now, we need to accept our returned package names in EditActivity:
1 2 3 4 5 6 7 8 9 10 |
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && requestCode == 1) { String apps = data.getStringExtra("apps"); TextView appsCountTextView = (TextView)this.findViewById(R.id.appsCountText); packages = apps; appsCountTextView.setText(String.valueOf(apps.length() > 0 ? apps.split("\\|").length : 0)); } } |
Here we compare request and result codes for running appropriate code. When we will get request code 1 and result code OK – we will try to get extra “apps” from the intent and set count of packages to the label.
Okay, when this is done, we need to save our setting:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
public void onAddButtonClick(View view) { EditText nameEdit = (EditText)this.findViewById(R.id.nameEditText); EditText titleEdit = (EditText)this.findViewById(R.id.titleEditText); EditText bodyEdit = (EditText)this.findViewById(R.id.bodyEditText); EditText excludeEdit = (EditText)this.findViewById(R.id.excludeEditText); CheckBox excludeRegexCheckbox = (CheckBox)this.findViewById(R.id.excludeRegexCheckBox); EditText removalDelayEdit = (EditText)this.findViewById(R.id.removalDelayText); DbHelper dbHelper = new DbHelper(this); String name = nameEdit.getText().toString(); String title = titleEdit.getText().toString(); String body = bodyEdit.getText().toString(); String exclude = excludeEdit.getText().toString(); String removalDelayString = removalDelayEdit.getText().toString(); int removalDelay = Integer.parseInt(removalDelayString.length() == 0 ? "0" : removalDelayString); if (name.length() == 0){ Snackbar.make(view, getString(R.string.name_shouldnot_be_empty) , Snackbar.LENGTH_LONG).show(); return; } if (!dbHelper.checkName(name, isEdit ? originalSetting.id : 0)){ Snackbar.make(view, getString(R.string.name_already_taken, name) , Snackbar.LENGTH_LONG).show(); return; } if (title.length() == 0){ Snackbar.make(view, getString(R.string.title_shouldnot_be_empty) , Snackbar.LENGTH_LONG).show(); return; } if (body.length() == 0){ Snackbar.make(view, getString(R.string.body_shouldnot_be_empty) , Snackbar.LENGTH_LONG).show(); return; } if (excludeRegexCheckbox.isChecked()){ try { Pattern.compile(exclude.toString()); } catch (PatternSyntaxException exception) { Snackbar.make(view, getString(R.string.wrong_regex) , Snackbar.LENGTH_LONG).show(); return; } } ResendSetting setting = new ResendSetting(0, name, true, packages, title, body, exclude, excludeRegexCheckbox.isChecked(), removalDelay); if (isEdit){ dbHelper.editSetting(originalSetting.id, setting); } else { dbHelper.addSetting(setting); } final Activity activity = this; Snackbar.make(view, getString(isEdit? R.string.setting_was_edited : R.string.setting_was_added, name), Snackbar.LENGTH_SHORT).setCallback(new Snackbar.Callback() { @Override public void onDismissed(Snackbar snackbar, int event) { activity.finish(); super.onDismissed(snackbar, event); } }).show(); } |
This is also pretty simple: try to validate all fields (if the meet requirements, for example, for name we should check if there is no another setting with same name), show Snackbar on errors, and if everything is okay – save new or edit existing setting in database, show message that setting was saved, and when Snackbar will be dismissed – close the activity:
1 2 3 4 5 6 7 |
Snackbar.make(view, getString(isEdit? R.string.setting_was_edited : R.string.setting_was_added, name), Snackbar.LENGTH_SHORT).setCallback(new Snackbar.Callback() { @Override public void onDismissed(Snackbar snackbar, int event) { activity.finish(); super.onDismissed(snackbar, event); } }).show(); |
Okay, the only one thing that left is to open this activity for adding or editing.
For adding a new setting, add following code to the MainActivity onCreate method:
1 2 3 4 5 6 7 8 |
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, EditActivity.class); startActivity(intent); } }); |
Here, on the floating button click callback we are creating a new Intent object, that shows wich activity to open and in which context, and calling startActivity method with intent as argument.
For editing it’s the same method, but we need to put our setting, which we want to edit, as an extra to the intent (ResendSettingArrayAdapter getView method):
1 2 3 4 5 6 7 8 |
nameTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(context, EditActivity.class); intent.putExtra("setting", objects.get(position)); context.startActivity(intent); } }); |
That’s it!
The only one thing is left in our application – to receive and process notifications. But it’s a content for the next, the last article in this series 🙂
Thanks!
P.S. #1 All source code you can find in my GitHub – https://github.com/dbondarchuk/Notification-Resender
P.S. #2 You can also download app for your bracelet here – https://github.com/dbondarchuk/Notification-Resender/releases