Java tutorial
/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.iosched.ui; import com.google.android.apps.iosched.R; import com.google.android.apps.iosched.provider.ScheduleContract.Blocks; import com.google.android.apps.iosched.provider.ScheduleContract.Rooms; import com.google.android.apps.iosched.provider.ScheduleContract.Sessions; import com.google.android.apps.iosched.provider.ScheduleContract.Speakers; import com.google.android.apps.iosched.provider.ScheduleContract.Tracks; import com.google.android.apps.iosched.provider.ScheduleContract.Vendors; import com.google.android.apps.iosched.service.SyncService; import com.google.android.apps.iosched.util.FractionalTouchDelegate; import com.google.android.apps.iosched.util.NotifyingAsyncQueryHandler; import com.google.android.apps.iosched.util.UIUtils; import com.google.android.apps.iosched.util.NotifyingAsyncQueryHandler.AsyncQueryListener; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.util.EntityUtils; import org.json.JSONObject; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.TabActivity; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.graphics.RectF; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.TabHost; import android.widget.TextView; import android.widget.CompoundButton.OnCheckedChangeListener; /** * {@link Activity} that displays details about a specific * {@link Sessions#SESSION_ID}, as requested through {@link Intent#getData()}. */ public class SessionDetailActivity extends TabActivity implements AsyncQueryListener, OnCheckedChangeListener { private static final String TAG = "SessionDetailActivity"; /** * Since {@link Sessions} can belong to multiple {@link Tracks}, the parent * {@link Activity} can send this extra specifying a {@link Tracks} * {@link Uri} that should be used for coloring the title-bar. */ public static final String EXTRA_TRACK = "com.google.android.iosched.extra.TRACK"; private static final String MODERATOR_PACKAGE = "com.google.android.apps.moderator"; private static final String TAG_SUMMARY = "summary"; private static final String TAG_NOTES = "notes"; private static final String TAG_MODERATOR = "moderator"; private String mSessionId; private Uri mSessionUri; private String mTitleString; private String mHashtag; private String mRoomId; private TextView mTitle; private TextView mSubtitle; private CompoundButton mStarred; private TextView mAbstract; private TextView mRequirements; private NotifyingAsyncQueryHandler mHandler; private boolean mSessionCursor = false; private boolean mSpeakersCursor = false; private boolean mHasSummaryContent = false; private Uri mModeratorUri; private Uri mWaveUri; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_session_detail); mTitle = (TextView) findViewById(R.id.session_title); mSubtitle = (TextView) findViewById(R.id.session_subtitle); mStarred = (CompoundButton) findViewById(R.id.star_button); mStarred.setFocusable(true); mStarred.setClickable(true); // Larger target triggers star toggle final View starParent = findViewById(R.id.list_item_session); FractionalTouchDelegate.setupDelegate(starParent, mStarred, new RectF(0.6f, 0f, 1f, 0.8f)); mAbstract = (TextView) findViewById(R.id.session_abstract); mRequirements = (TextView) findViewById(R.id.session_requirements); final Intent intent = getIntent(); mSessionUri = intent.getData(); mSessionId = Sessions.getSessionId(mSessionUri); setupSummaryTab(); setupNotesTab(); // Start background queries to load session and track details final Uri trackUri = resolveTrackUri(intent); final Uri speakersUri = Sessions.buildSpeakersDirUri(mSessionId); mHandler = new NotifyingAsyncQueryHandler(getContentResolver(), this); mHandler.startQuery(SessionsQuery._TOKEN, mSessionUri, SessionsQuery.PROJECTION); mHandler.startQuery(TracksQuery._TOKEN, trackUri, TracksQuery.PROJECTION); mHandler.startQuery(SpeakersQuery._TOKEN, speakersUri, SpeakersQuery.PROJECTION); } /** Build and add "summary" tab. */ private void setupSummaryTab() { final TabHost host = getTabHost(); // Summary content comes from existing layout host.addTab(host.newTabSpec(TAG_SUMMARY).setIndicator(buildIndicator(R.string.session_summary)) .setContent(R.id.tab_session_summary)); } /** Build and add "notes" tab. */ private void setupNotesTab() { final TabHost host = getTabHost(); final Uri notesUri = Sessions.buildNotesDirUri(mSessionId); final Intent intent = new Intent(Intent.ACTION_VIEW, notesUri); intent.addCategory(Intent.CATEGORY_TAB); intent.putExtra(NotesActivity.EXTRA_SHOW_INSERT, true); // Notes content comes from reused activity host.addTab( host.newTabSpec(TAG_NOTES).setIndicator(buildIndicator(R.string.session_notes)).setContent(intent)); } /** Build and add "moderator" tab. */ private void setupModeratorTab(Cursor sessionsCursor) { final TabHost host = getTabHost(); // Insert Moderator when available final View moderatorBlock = findViewById(R.id.moderator_block); final String moderatorLink = sessionsCursor.getString(SessionsQuery.MODERATOR_LINK); final boolean validModerator = !TextUtils.isEmpty(moderatorLink); if (validModerator) { mModeratorUri = Uri.parse(moderatorLink); // Set link, but handle clicks manually final TextView textView = (TextView) findViewById(R.id.moderator_link); textView.setText(mModeratorUri.toString()); textView.setMovementMethod(null); textView.setClickable(true); textView.setFocusable(true); // Start background fetch of moderator status startModeratorStatusFetch(moderatorLink); moderatorBlock.setVisibility(View.VISIBLE); } else { moderatorBlock.setVisibility(View.GONE); } // Insert Wave when available final View waveBlock = findViewById(R.id.wave_block); final String waveLink = sessionsCursor.getString(SessionsQuery.WAVE_LINK); final boolean validWave = !TextUtils.isEmpty(waveLink); if (validWave) { // Rewrite incoming Wave URL to punch through user-agent check mWaveUri = Uri.parse(waveLink).buildUpon().appendQueryParameter("nouacheck", "1").build(); // Set link, but handle clicks manually final TextView textView = (TextView) findViewById(R.id.wave_link); textView.setText(mWaveUri.toString()); textView.setMovementMethod(null); textView.setClickable(true); textView.setFocusable(true); waveBlock.setVisibility(View.VISIBLE); } else { waveBlock.setVisibility(View.GONE); } if (validModerator || validWave) { // Moderator content comes from existing layout host.addTab(host.newTabSpec(TAG_MODERATOR).setIndicator(buildIndicator(R.string.session_interact)) .setContent(R.id.tab_session_moderator)); } } /** Handle Moderator link. */ public void onModeratorClick(View v) { boolean appPresent = false; try { final PackageManager pm = getPackageManager(); final ApplicationInfo ai = pm.getApplicationInfo(MODERATOR_PACKAGE, 0); if (ai != null) appPresent = true; } catch (NameNotFoundException e) { Log.w(TAG, "Problem searching for moderator: " + e.toString()); } if (appPresent) { // Directly launch intent when already installed startModerator(); } else { // Otherwise suggest installing app from Market showDialog(R.id.dialog_moderator); } } /** Handle Wave link. */ public void onWaveClick(View v) { showDialog(R.id.dialog_wave); } @Override protected Dialog onCreateDialog(int id) { switch (id) { case R.id.dialog_moderator: { return new AlertDialog.Builder(this).setTitle(R.string.dialog_moderator_title) .setIcon(android.R.drawable.ic_dialog_info).setMessage(R.string.dialog_moderator_message) .setNegativeButton(R.string.dialog_moderator_market, new ModeratorMarketClickListener()) .setPositiveButton(R.string.dialog_moderator_web, new ModeratorStartClickListener()) .setCancelable(true).create(); } case R.id.dialog_wave: { return new AlertDialog.Builder(this).setTitle(R.string.dialog_wave_title) .setIcon(android.R.drawable.ic_dialog_info).setMessage(R.string.dialog_wave_message) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, new WaveConfirmClickListener()).setCancelable(true) .create(); } } return null; } private class ModeratorStartClickListener implements DialogInterface.OnClickListener { public void onClick(DialogInterface dialog, int which) { startModerator(); } } private class ModeratorMarketClickListener implements DialogInterface.OnClickListener { public void onClick(DialogInterface dialog, int which) { final Uri marketUri = Uri.parse("http://market.android.com/search?q=pname:" + MODERATOR_PACKAGE); startActivity(new Intent(Intent.ACTION_VIEW, marketUri)); } } private class WaveConfirmClickListener implements DialogInterface.OnClickListener { public void onClick(DialogInterface dialog, int which) { startWave(); } } private void startModerator() { startActivity(new Intent(Intent.ACTION_VIEW, mModeratorUri)); } private void startWave() { startActivity(new Intent(Intent.ACTION_VIEW, mWaveUri)); } /** * Build a {@link View} to be used as a tab indicator, setting the requested * string resource as its label. */ private View buildIndicator(int textRes) { final TextView indicator = (TextView) getLayoutInflater().inflate(R.layout.tab_indicator, getTabWidget(), false); indicator.setText(textRes); return indicator; } /** * Derive {@link Tracks#CONTENT_ITEM_TYPE} {@link Uri} based on incoming * {@link Intent}, using {@link #EXTRA_TRACK} when set. */ private Uri resolveTrackUri(Intent intent) { final Uri trackUri = intent.getParcelableExtra(EXTRA_TRACK); if (trackUri != null) { return trackUri; } else { return Sessions.buildTracksDirUri(mSessionId); } } /** {@inheritDoc} */ public void onQueryComplete(int token, Object cookie, Cursor cursor) { if (token == SessionsQuery._TOKEN) { onSessionQueryComplete(cursor); } else if (token == TracksQuery._TOKEN) { onTrackQueryComplete(cursor); } else if (token == SpeakersQuery._TOKEN) { onSpeakersQueryComplete(cursor); } else { cursor.close(); } } /** Handle {@link SessionsQuery} {@link Cursor}. */ private void onSessionQueryComplete(Cursor cursor) { try { mSessionCursor = true; if (!cursor.moveToFirst()) return; // Format time block this session occupies final long blockStart = cursor.getLong(SessionsQuery.BLOCK_START); final long blockEnd = cursor.getLong(SessionsQuery.BLOCK_END); final String roomName = cursor.getString(SessionsQuery.ROOM_NAME); final String subtitle = UIUtils.formatSessionSubtitle(blockStart, blockEnd, roomName, this); mTitleString = cursor.getString(SessionsQuery.TITLE); mTitle.setText(mTitleString); mSubtitle.setText(subtitle); mHashtag = cursor.getString(SessionsQuery.HASHTAG); if (TextUtils.isEmpty(mHashtag)) mHashtag = ""; mRoomId = cursor.getString(SessionsQuery.ROOM_ID); // Unregister around setting checked state to avoid triggering // listener since change isn't user generated. mStarred.setOnCheckedChangeListener(null); mStarred.setChecked(cursor.getInt(SessionsQuery.STARRED) != 0); mStarred.setOnCheckedChangeListener(this); final String sessionAbstract = cursor.getString(SessionsQuery.ABSTRACT); if (!TextUtils.isEmpty(sessionAbstract)) { UIUtils.setTextMaybeHtml(mAbstract, sessionAbstract); mAbstract.setVisibility(View.VISIBLE); mHasSummaryContent = true; } else { mAbstract.setVisibility(View.GONE); } final View requirementsBlock = findViewById(R.id.session_requirements_block); final String sessionRequirements = cursor.getString(SessionsQuery.REQUIREMENTS); if (!TextUtils.isEmpty(sessionRequirements)) { UIUtils.setTextMaybeHtml(mRequirements, sessionRequirements); requirementsBlock.setVisibility(View.VISIBLE); mHasSummaryContent = true; } else { requirementsBlock.setVisibility(View.GONE); } setupModeratorTab(cursor); // Show empty message when all data is loaded, and nothing to show if (mSpeakersCursor && !mHasSummaryContent) { findViewById(android.R.id.empty).setVisibility(View.VISIBLE); } } finally { cursor.close(); } } /** Handle {@link TracksQuery} {@link Cursor}. */ private void onTrackQueryComplete(Cursor cursor) { try { if (!cursor.moveToFirst()) return; // Use found track to build title-bar ((TextView) findViewById(R.id.title_text)).setText(cursor.getString(TracksQuery.TRACK_NAME)); UIUtils.setTitleBarColor(findViewById(R.id.title_container), cursor.getInt(TracksQuery.TRACK_COLOR)); } finally { cursor.close(); } } private void onSpeakersQueryComplete(Cursor cursor) { try { mSpeakersCursor = true; // TODO: remove any existing speakers from layout, since this cursor // might be from a data change notification. final ViewGroup speakersGroup = (ViewGroup) findViewById(R.id.session_speakers_block); final LayoutInflater inflater = getLayoutInflater(); boolean hasSpeakers = false; while (cursor.moveToNext()) { final String speakerName = cursor.getString(SpeakersQuery.SPEAKER_NAME); final String speakerCompany = cursor.getString(SpeakersQuery.SPEAKER_COMPANY); if (TextUtils.isEmpty(speakerName)) continue; final View speakerView = inflater.inflate(R.layout.speaker_detail, speakersGroup, false); final String speaker = getString(R.string.speaker_template, speakerName, speakerCompany); ((TextView) speakerView.findViewById(R.id.speaker_header)).setText(speaker); final String speakerAbstract = cursor.getString(SpeakersQuery.SPEAKER_ABSTRACT); final TextView abstractView = (TextView) speakerView.findViewById(R.id.speaker_abstract); UIUtils.setTextMaybeHtml(abstractView, speakerAbstract); speakersGroup.addView(speakerView); hasSpeakers = true; mHasSummaryContent = true; } speakersGroup.setVisibility(hasSpeakers ? View.VISIBLE : View.GONE); // Show empty message when all data is loaded, and nothing to show if (mSessionCursor && !mHasSummaryContent) { findViewById(android.R.id.empty).setVisibility(View.VISIBLE); } } finally { cursor.close(); } } /** Handle "home" title-bar action. */ public void onHomeClick(View v) { UIUtils.goHome(this); } /** Handle "share" title-bar action. */ public void onShareClick(View v) { // TODO: consider bringing in shortlink to session final String shareString = getString(R.string.share_template, mTitleString, mHashtag); final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, shareString); startActivity(Intent.createChooser(intent, getText(R.string.title_share))); } /** Handle "map" title-bar action. */ public void onMapClick(View v) { final Intent intent = new Intent(this, MapActivity.class); if (mRoomId != null && mRoomId.startsWith("officehours")) { intent.putExtra(MapActivity.EXTRA_ROOM, MapActivity.OFFICE_HOURS_ROOM_ID); } else { intent.putExtra(MapActivity.EXTRA_ROOM, mRoomId); } startActivity(intent); } /** Handle "search" title-bar action. */ public void onSearchClick(View v) { UIUtils.goSearch(this); } /** Handle toggling of starred checkbox. */ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { final ContentValues values = new ContentValues(); values.put(Vendors.STARRED, isChecked ? 1 : 0); mHandler.startUpdate(mSessionUri, values); } private static HttpClient sHttpClient; private static synchronized HttpClient getHttpClient(Context context) { if (sHttpClient == null) { sHttpClient = SyncService.getHttpClient(context); } return sHttpClient; } private void startModeratorStatusFetch(String moderatorLink) { try { // Extract series and topic from moderator link // NOTE: this makes some ugly assumptions that the "t=" parameter // occurs at the end of the URL. final String clause = moderatorLink.substring(moderatorLink.indexOf("t=") + 2); final int dotIndex = clause.indexOf('.'); final int seriesId = Integer.parseInt(clause.substring(0, dotIndex), 16); final int topicId = Integer.parseInt(clause.substring(dotIndex + 1), 16); // Kick off background request to find current status final String remoteUrl = "http://www.googleapis.com/moderator/v1/series/" + seriesId + "/topics/" + topicId; new ModeratorStatusTask().execute(remoteUrl); } catch (Exception e) { Log.w(TAG, "Problem while parsing Moderator URL", e); } } private class ModeratorStatusTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... params) { final String param = params[0]; try { final Context context = SessionDetailActivity.this; final HttpClient httpClient = getHttpClient(context); final HttpResponse resp = httpClient.execute(new HttpGet(param)); final HttpEntity entity = resp.getEntity(); final int statusCode = resp.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK || entity == null) return null; final String respString = EntityUtils.toString(entity); final JSONObject respJson = new JSONObject(respString); final JSONObject data = respJson.getJSONObject("data"); final JSONObject counters = respJson.getJSONObject("counters"); final int questions = counters.getInt("submissions"); final int votes = counters.getInt("noteVotes") + counters.getInt("plusVotes") + counters.getInt("minusVotes"); return getString(R.string.session_moderator_status, questions, votes); } catch (Exception e) { Log.w(TAG, "Problem while loading Moderator status: " + e.toString()); } return null; } @Override protected void onPostExecute(String result) { final TextView status = (TextView) findViewById(R.id.moderator_status); if (result == null) { status.setVisibility(View.GONE); } else { status.setVisibility(View.VISIBLE); status.setText(result); } } } /** {@link Sessions} query parameters. */ private interface SessionsQuery { int _TOKEN = 0x1; String[] PROJECTION = { Blocks.BLOCK_START, Blocks.BLOCK_END, Sessions.TYPE, Sessions.TITLE, Sessions.ABSTRACT, Sessions.REQUIREMENTS, Sessions.STARRED, Sessions.MODERATOR_URL, Sessions.WAVE_URL, Sessions.ROOM_ID, Sessions.HASHTAG, Rooms.ROOM_NAME, }; int BLOCK_START = 0; int BLOCK_END = 1; int TYPE = 2; int TITLE = 3; int ABSTRACT = 4; int REQUIREMENTS = 5; int STARRED = 6; int MODERATOR_LINK = 7; int WAVE_LINK = 8; int ROOM_ID = 9; int HASHTAG = 10; int ROOM_NAME = 11; } /** {@link Tracks} query parameters. */ private interface TracksQuery { int _TOKEN = 0x2; String[] PROJECTION = { Tracks.TRACK_NAME, Tracks.TRACK_COLOR, }; int TRACK_NAME = 0; int TRACK_COLOR = 1; } private interface SpeakersQuery { int _TOKEN = 0x3; String[] PROJECTION = { Speakers.SPEAKER_NAME, Speakers.SPEAKER_COMPANY, Speakers.SPEAKER_ABSTRACT, }; int SPEAKER_NAME = 0; int SPEAKER_COMPANY = 1; int SPEAKER_ABSTRACT = 2; } }