Java tutorial
/** * Copyright (C) 2015 Jon Griffiths (jon_p_griffiths@yahoo.com) * Copyright (C) 2013 Dominik Schrmann <dominik@dominikschuermann.de> * Copyright (C) 2010-2011 Lukas Aichbauer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.sufficientlysecure.ical.ui; import java.io.File; import java.io.InputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.util.CompatibilityHints; import org.apache.commons.codec.binary.Base64; import org.sufficientlysecure.ical.AndroidCalendar; import org.sufficientlysecure.ical.ProcessVEvent; import org.sufficientlysecure.ical.SaveCalendar; import org.sufficientlysecure.ical.Settings; import org.sufficientlysecure.ical.R; import org.sufficientlysecure.ical.ui.dialogs.DialogTools; import org.sufficientlysecure.ical.ui.dialogs.RunnableWithProgress; import org.sufficientlysecure.ical.util.Log; import android.Manifest; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentActivity; import android.support.v4.content.ContextCompat; import android.text.Html; import android.text.method.LinkMovementMethod; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.ScrollView; import android.widget.Toast; import android.widget.TextView; public class MainActivity extends FragmentActivity implements View.OnClickListener { private static final String TAG = "ICS_MainActivity"; public static final String LOAD_CALENDAR = "org.sufficientlysecure.ical.LOAD_CALENDAR"; public static final String EXTRA_CALENDAR_ID = "calendarId"; private static final int MY_PERMISSIONS_REQUEST = 1; private static final String[] MY_PERMISSIONS = new String[] { Manifest.permission.GET_ACCOUNTS, Manifest.permission.READ_CALENDAR, Manifest.permission.READ_EXTERNAL_STORAGE }; private Settings mSettings; private CalendarBuilder mCalendarBuilder; private Calendar mCalendar; private static final long NO_CALENDAR = -1; private long mIntentCalendarId = NO_CALENDAR; private boolean mInitialCreated = false; private IntentFilter mCalendarUpdateFilter; private BroadcastReceiver mCalendarUpdateReciever; // UID generation private long mUidMs = 0; private String mUidTail; // Views private Spinner mCalendarSpinner; private Spinner mFileSpinner; private Button mLoadButton; private Button mInsertButton; private Button mDeleteButton; private Button mExportButton; private TextView mTextCalName; private TextView mTextCalAccountName; private TextView mTextCalAccountType; private TextView mTextCalOwner; private TextView mTextCalState; private TextView mTextCalId; private TextView mTextCalTimezone; private TextView mTextCalSize; // Values private List<AndroidCalendar> mCalendars; private ScrollView mScrollViewMain; private LinearLayout mInsertDeleteLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mIntentCalendarId = NO_CALENDAR; mInitialCreated = true; // Create a receiver for calendar updates. mCalendarUpdateFilter = new IntentFilter("android.intent.action.PROVIDER_CHANGED"); mCalendarUpdateReciever = new BroadcastReceiver() { public void onReceive(final Context context, final Intent intent) { Log.d(TAG, "Received broadcast: " + intent.getAction()); if (intent.getAction() == mCalendarUpdateFilter.getAction(0)) onExternalCalendarChanged(); } }; initView(); if (hasPermissions()) initIntent(); } private boolean isGranted(final String permission) { final int status = ContextCompat.checkSelfPermission(this, permission); return status == PackageManager.PERMISSION_GRANTED; } private boolean hasPermissions() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true; boolean allGranted = true; for (final String permission : MY_PERMISSIONS) if (!isGranted(permission)) { allGranted = false; break; } if (allGranted) return true; ActivityCompat.requestPermissions(this, MY_PERMISSIONS, MY_PERMISSIONS_REQUEST); return false; } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case MY_PERMISSIONS_REQUEST: { boolean allGranted = false; if (grantResults.length > 0) { allGranted = true; for (int grantResult : grantResults) if (grantResult != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (!allGranted) { Toast.makeText(this, R.string.permissions_not_granted, Toast.LENGTH_LONG).show(); finish(); return; } initIntent(); } } } private void initView() { mSettings = new Settings(PreferenceManager.getDefaultSharedPreferences(this)); SettingsActivity.processSettings(mSettings); // Retrieve views mCalendarSpinner = (Spinner) findViewById(R.id.SpinnerChooseCalendar); AdapterView.OnItemSelectedListener calListener; calListener = new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { AndroidCalendar calendar = mCalendars.get(pos); mTextCalName.setText(calendar.mName); mTextCalAccountName.setText(calendar.mAccountName); mTextCalAccountType.setText(calendar.mAccountType); mTextCalOwner.setText(calendar.mOwner); mTextCalState.setText(calendar.mIsActive ? R.string.active : R.string.inactive); mTextCalId.setText(calendar.mIdStr); if (calendar.mTimezone == null) mTextCalTimezone.setText(R.string.not_applicable); else mTextCalTimezone.setText(calendar.mTimezone); mSettings.putLong(Settings.PREF_LASTCALENDARID, calendar.mId); mSettings.putString(Settings.PREF_LASTCALENDARNAME, calendar.mName); updateNumEntries(calendar); } @Override public void onNothingSelected(AdapterView<?> arg0) { } }; mCalendarSpinner.setOnItemSelectedListener(calListener); mFileSpinner = (Spinner) findViewById(R.id.SpinnerFile); AdapterView.OnItemSelectedListener fileListener; fileListener = new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { mInsertDeleteLayout.setVisibility(View.GONE); } @Override public void onNothingSelected(AdapterView<?> arg0) { } }; mFileSpinner.setOnItemSelectedListener(fileListener); setupButton(R.id.SearchButton); mLoadButton = setupButton(R.id.LoadButton); mInsertButton = setupButton(R.id.InsertButton); mDeleteButton = setupButton(R.id.DeleteButton); mExportButton = setupButton(R.id.SaveButton); mScrollViewMain = (ScrollView) findViewById(R.id.ScrollViewMain); mInsertDeleteLayout = (LinearLayout) findViewById(R.id.InsertDeleteLayout); setupButton(R.id.SetUrlButton); mTextCalName = (TextView) findViewById(R.id.TextCalName); mTextCalAccountName = (TextView) findViewById(R.id.TextCalAccountName); mTextCalAccountType = (TextView) findViewById(R.id.TextCalAccountType); mTextCalOwner = (TextView) findViewById(R.id.TextCalOwner); mTextCalState = (TextView) findViewById(R.id.TextCalState); mTextCalId = (TextView) findViewById(R.id.TextCalId); mTextCalTimezone = (TextView) findViewById(R.id.TextCalTimezone); mTextCalSize = (TextView) findViewById(R.id.TextCalSize); } private void initIntent() { Intent intent = getIntent(); if (intent == null) return; String action = intent.getAction(); if (action.equals(LOAD_CALENDAR)) mIntentCalendarId = intent.getLongExtra(EXTRA_CALENDAR_ID, NO_CALENDAR); onExternalCalendarChanged(); if (action.equals(Intent.ACTION_VIEW)) setSource(null, intent.getData(), null, null); // File intent } public Settings getSettings() { return mSettings; } private boolean isListUpdate(final List<AndroidCalendar> calendars) { if (mCalendars == null) { Log.d(TAG, "First time init of calendar list"); return true; } if (mCalendars.size() != calendars.size()) { Log.i(TAG, "A calendar has been added or removed"); return true; } for (int i = 0; i < mCalendars.size(); ++i) { if (mCalendars.get(i).differsFrom(calendars.get(i))) { Log.i(TAG, "A calendar or its events has changed"); return true; } } Log.d(TAG, "No calendar changes found"); return false; // No differences } private void initialiseCalendars() { Log.d(TAG, "initialiseCalendars"); List<AndroidCalendar> calendars = AndroidCalendar.loadAll(getContentResolver()); if (calendars.isEmpty()) { Runnable task; task = new Runnable() { public void run() { DialogInterface.OnClickListener okTask; okTask = new DialogInterface.OnClickListener() { public void onClick(DialogInterface iface, int id) { iface.cancel(); MainActivity.this.finish(); } }; new AlertDialog.Builder(MainActivity.this).setMessage(R.string.no_calendars_found) .setIcon(R.mipmap.ic_launcher).setTitle(R.string.information).setCancelable(false) .setPositiveButton(android.R.string.ok, okTask).create().show(); } }; runOnUiThread(task); } if (!isListUpdate(calendars)) return; // FIXME: If we already have initialised then: // a) Preserve our chosen calendar selection if it still exists mCalendars = calendars; setupSpinner(mCalendarSpinner, mCalendars, mExportButton); String calendarName = null; if (mIntentCalendarId == NO_CALENDAR) { // Not loading from an Intent: use the previously selected calendar mIntentCalendarId = mSettings.getLong(mSettings.PREF_LASTCALENDARID, NO_CALENDAR); if (mIntentCalendarId != NO_CALENDAR) calendarName = mSettings.getString(mSettings.PREF_LASTCALENDARNAME); } boolean found = false; for (int i = 0; i < mCalendars.size(); i++) { if (mCalendars.get(i).mId == mIntentCalendarId && (calendarName == null || mCalendars.get(i).mName.contentEquals(calendarName))) { found = true; final int index = i; runOnUiThread(new Runnable() { public void run() { mCalendarSpinner.setSelection(index); } }); break; } } if (!found) mIntentCalendarId = NO_CALENDAR; // Don't try to match this id again } public void showToast(final String msg) { runOnUiThread(new Runnable() { public void run() { Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show(); } }); } public void updateNumEntries(AndroidCalendar calendar) { final int entries = calendar.mNumEntries; runOnUiThread(new Runnable() { public void run() { mTextCalSize.setText(Integer.toString(entries)); mExportButton.setEnabled(entries > 0); mInsertDeleteLayout.setVisibility(View.GONE); } }); } private Button setupButton(int id) { Button button = (Button) findViewById(id); button.setOnClickListener(this); return button; } private <E> void setupSpinner(final Spinner spinner, final List<E> list, final Button button) { final int id = android.R.layout.simple_spinner_item; final int dropId = android.R.layout.simple_spinner_dropdown_item; final Context ctx = this; runOnUiThread(new Runnable() { public void run() { ArrayAdapter<E> adaptor = new ArrayAdapter<>(ctx, id, list); adaptor.setDropDownViewResource(dropId); spinner.setAdapter(adaptor); if (list.size() != 0) spinner.setVisibility(View.VISIBLE); button.setVisibility(View.VISIBLE); } }); } private void setSources(List<CalendarSource> sources) { setupSpinner(mFileSpinner, sources, mLoadButton); } protected void onResume() { super.onResume(); Log.d(TAG, "onResume"); if (mInitialCreated) mInitialCreated = false; // Init already done by onCreate() else onExternalCalendarChanged(); registerReceiver(mCalendarUpdateReciever, mCalendarUpdateFilter); } protected void onPause() { super.onPause(); Log.d(TAG, "onPause"); unregisterReceiver(mCalendarUpdateReciever); } private void onExternalCalendarChanged() { new Thread(new Runnable() { public void run() { // Update view if any source calendar was modified MainActivity.this.initialiseCalendars(); } }).start(); } public boolean setSource(String url, Uri uri, String username, String password) { try { CalendarSource source = new CalendarSource(url, uri, username, password); setSources(Collections.singletonList(source)); return true; } catch (Exception e) { return false; } } public AndroidCalendar getSelectedCalendar() { return (AndroidCalendar) mCalendarSpinner.getSelectedItem(); } public InputStream getSelectedURI() throws IOException { CalendarSource sel = (CalendarSource) mFileSpinner.getSelectedItem(); return sel == null ? null : sel.getStream(); } public String generateUid() { // Generated UIDs take the form <ms>-<uuid>@sufficientlysecure.org. if (mUidTail == null) { String uidPid = mSettings.getString(Settings.PREF_UIDPID); if (uidPid.length() == 0) { uidPid = UUID.randomUUID().toString().replace("-", ""); mSettings.putString(Settings.PREF_UIDPID, uidPid); } mUidTail = uidPid + "@sufficientlysecure.org"; } long ms = System.currentTimeMillis(); if (mUidMs == ms) ms++; // Force ms to be unique mUidMs = ms; return Long.toString(ms) + mUidTail; } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.help: DialogTools.info(this, R.string.help, Html.fromHtml(getString(R.string.help_html))); break; case R.id.settings: // Show our Settings view startActivity(new Intent(this, SettingsActivity.class)); break; case R.id.legal_notices: showLegalNotices(); break; default: return super.onContextItemSelected(item); } return true; } private void showLegalNotices() { TextView text = new TextView(this); text.setText(Html.fromHtml(getString(R.string.legal_notices_html))); text.setMovementMethod(LinkMovementMethod.getInstance()); new AlertDialog.Builder(this).setView(text).create().show(); } private class CalendarSource { private static final String HTTP_SEP = "://"; private URL mUrl = null; private Uri mUri = null; private final String mString; private final String mUsername; private final String mPassword; public CalendarSource(String url, Uri uri, String username, String password) throws MalformedURLException { if (url != null) { mUrl = new URL(url); mString = mUrl.toString(); } else { mUri = uri; mString = uri.toString(); } mUsername = username; mPassword = password; } public URLConnection getConnection() throws IOException { if (mUsername != null) { String protocol = mUrl.getProtocol(); String userPass = mUsername + ":" + mPassword; if (protocol.equalsIgnoreCase("ftp") || protocol.equalsIgnoreCase("ftps")) { String external = mUrl.toExternalForm(); String end = external.substring(protocol.length() + HTTP_SEP.length()); return new URL(protocol + HTTP_SEP + userPass + "@" + end).openConnection(); } if (protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")) { String encoded = new String(new Base64().encode(userPass.getBytes("UTF-8"))); URLConnection connection = mUrl.openConnection(); connection.setRequestProperty("Authorization", "Basic " + encoded); return connection; } } return mUrl.openConnection(); } public InputStream getStream() throws IOException { if (mUri != null) return getContentResolver().openInputStream(mUri); URLConnection c = this.getConnection(); return c == null ? null : c.getInputStream(); } @Override public String toString() { return mString; } } private class SearchForFiles extends RunnableWithProgress { public SearchForFiles(MainActivity activity) { super(activity, R.string.searching_for_files, false); } private void search(File root, List<CalendarSource> sources, String... extensions) { if (!root.isFile()) { File[] children = root.listFiles(); if (children == null) return; for (File file : children) search(file, sources, extensions); } for (String ext : extensions) { if (root.toString().endsWith(ext)) { try { final String url = root.toURI().toURL().toString(); sources.add(new CalendarSource(url, null, null, null)); } catch (MalformedURLException e) { // Can't happen } } } } @Override protected void run() throws Exception { List<CalendarSource> sources = new ArrayList<>(); search(Environment.getExternalStorageDirectory(), sources, "ics", "ical", "icalendar"); getActivity().setSources(sources); } } private class LoadFile extends RunnableWithProgress { public LoadFile(MainActivity activity) { super(activity, R.string.reading_file_please_wait, false); } private void setHint(String key, boolean value) { CompatibilityHints.setHintEnabled(key, value); } @Override protected void run() throws Exception { setHint(CompatibilityHints.KEY_RELAXED_UNFOLDING, mSettings.getIcal4jUnfoldingRelaxed()); setHint(CompatibilityHints.KEY_RELAXED_PARSING, mSettings.getIcal4jParsingRelaxed()); setHint(CompatibilityHints.KEY_RELAXED_VALIDATION, mSettings.getIcal4jValidationRelaxed()); setHint(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, mSettings.getIcal4jCompatibilityOutlook()); setHint(CompatibilityHints.KEY_NOTES_COMPATIBILITY, mSettings.getIcal4jCompatibilityNotes()); setHint(CompatibilityHints.KEY_VCARD_COMPATIBILITY, mSettings.getIcal4jCompatibilityVcard()); if (mCalendarBuilder == null) mCalendarBuilder = new CalendarBuilder(); mCalendar = mCalendarBuilder.build(getSelectedURI()); runOnUiThread(new Runnable() { public void run() { if (mCalendar == null) { mInsertDeleteLayout.setVisibility(View.GONE); return; } Resources res = getResources(); final int n = mCalendar.getComponents(VEvent.VEVENT).size(); mInsertButton.setText(get(res, R.plurals.insert_n_entries, n)); mDeleteButton.setText(get(res, R.plurals.delete_n_entries, n)); mInsertDeleteLayout.setVisibility(View.VISIBLE); mScrollViewMain.post(new Runnable() { @Override public void run() { mScrollViewMain.fullScroll(ScrollView.FOCUS_DOWN); } }); } private String get(Resources res, int id, int n) { return res.getQuantityString(id, n, n); } }); } } @Override public void onClick(View view) { switch (view.getId()) { case R.id.SetUrlButton: UrlDialog.show(this); break; case R.id.SearchButton: new SearchForFiles(this).start(); break; case R.id.LoadButton: new LoadFile(this).start(); break; case R.id.SaveButton: new SaveCalendar(this).start(); break; case R.id.InsertButton: case R.id.DeleteButton: new ProcessVEvent(this, mCalendar, view.getId() == R.id.InsertButton).start(); break; } } }