1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.hcgallery; 18 19 import android.app.ActionBar; 20 import android.app.Fragment; 21 import android.content.ClipData; 22 import android.content.ClipData.Item; 23 import android.content.ClipDescription; 24 import android.content.Intent; 25 import android.graphics.Bitmap; 26 import android.graphics.Color; 27 import android.net.Uri; 28 import android.os.AsyncTask; 29 import android.os.Bundle; 30 import android.view.ActionMode; 31 import android.view.DragEvent; 32 import android.view.LayoutInflater; 33 import android.view.Menu; 34 import android.view.MenuInflater; 35 import android.view.MenuItem; 36 import android.view.View; 37 import android.view.View.OnClickListener; 38 import android.view.ViewGroup; 39 import android.view.Window; 40 import android.view.WindowManager; 41 import android.widget.ImageView; 42 import android.widget.Toast; 43 44 import java.io.File; 45 import java.io.FileNotFoundException; 46 import java.io.FileOutputStream; 47 import java.io.IOException; 48 import java.util.StringTokenizer; 49 50 /** Fragment that shows the content selected from the TitlesFragment. 51 * When running on a screen size smaller than "large", this fragment is hosted in 52 * ContentActivity. Otherwise, it appears side by side with the TitlesFragment 53 * in MainActivity. */ 54 public class ContentFragment extends Fragment { 55 private View mContentView; 56 private int mCategory = 0; 57 private int mCurPosition = 0; 58 private boolean mSystemUiVisible = true; 59 private boolean mSoloFragment = false; 60 61 // The bitmap currently used by ImageView 62 private Bitmap mBitmap = null; 63 64 // Current action mode (contextual action bar, a.k.a. CAB) 65 private ActionMode mCurrentActionMode; 66 67 /** This is where we initialize the fragment's UI and attach some 68 * event listeners to UI components. 69 */ 70 @Override 71 public View onCreateView(LayoutInflater inflater, ViewGroup container, 72 Bundle savedInstanceState) { 73 mContentView = inflater.inflate(R.layout.content_welcome, null); 74 final ImageView imageView = (ImageView) mContentView.findViewById(R.id.image); 75 mContentView.setDrawingCacheEnabled(false); 76 77 // Handle drag events when a list item is dragged into the view 78 mContentView.setOnDragListener(new View.OnDragListener() { 79 public boolean onDrag(View view, DragEvent event) { 80 switch (event.getAction()) { 81 case DragEvent.ACTION_DRAG_ENTERED: 82 view.setBackgroundColor( 83 getResources().getColor(R.color.drag_active_color)); 84 break; 85 86 case DragEvent.ACTION_DRAG_EXITED: 87 view.setBackgroundColor(Color.TRANSPARENT); 88 break; 89 90 case DragEvent.ACTION_DRAG_STARTED: 91 return processDragStarted(event); 92 93 case DragEvent.ACTION_DROP: 94 view.setBackgroundColor(Color.TRANSPARENT); 95 return processDrop(event, imageView); 96 } 97 return false; 98 } 99 }); 100 101 // Show/hide the system status bar when single-clicking a photo. 102 mContentView.setOnClickListener(new OnClickListener() { 103 public void onClick(View view) { 104 if (mCurrentActionMode != null) { 105 // If we're in an action mode, don't toggle the action bar 106 return; 107 } 108 109 if (mSystemUiVisible) { 110 setSystemUiVisible(false); 111 } else { 112 setSystemUiVisible(true); 113 } 114 } 115 }); 116 117 // When long-pressing a photo, activate the action mode for selection, showing the 118 // contextual action bar (CAB). 119 mContentView.setOnLongClickListener(new View.OnLongClickListener() { 120 public boolean onLongClick(View view) { 121 if (mCurrentActionMode != null) { 122 return false; 123 } 124 125 mCurrentActionMode = getActivity().startActionMode( 126 mContentSelectionActionModeCallback); 127 view.setSelected(true); 128 return true; 129 } 130 }); 131 132 return mContentView; 133 } 134 135 /** This is where we perform additional setup for the fragment that's either 136 * not related to the fragment's layout or must be done after the layout is drawn. 137 */ 138 @Override 139 public void onActivityCreated(Bundle savedInstanceState) { 140 super.onActivityCreated(savedInstanceState); 141 142 // Set member variable for whether this fragment is the only one in the activity 143 Fragment listFragment = getFragmentManager().findFragmentById(R.id.titles_frag); 144 mSoloFragment = listFragment == null ? true : false; 145 146 if (mSoloFragment) { 147 // The fragment is alone, so enable up navigation 148 getActivity().getActionBar().setDisplayHomeAsUpEnabled(true); 149 // Must call in order to get callback to onOptionsItemSelected() 150 setHasOptionsMenu(true); 151 } 152 153 // Current position and UI visibility should survive screen rotations. 154 if (savedInstanceState != null) { 155 setSystemUiVisible(savedInstanceState.getBoolean("systemUiVisible")); 156 if (mSoloFragment) { 157 // Restoring these members is not necessary when this fragment 158 // is combined with the TitlesFragment, because when the TitlesFragment 159 // is restored, it selects the appropriate item and sends the event 160 // to the updateContentAndRecycleBitmap() method itself 161 mCategory = savedInstanceState.getInt("category"); 162 mCurPosition = savedInstanceState.getInt("listPosition"); 163 updateContentAndRecycleBitmap(mCategory, mCurPosition); 164 } 165 } 166 167 if (mSoloFragment) { 168 String title = Directory.getCategory(mCategory).getEntry(mCurPosition).getName(); 169 ActionBar bar = getActivity().getActionBar(); 170 bar.setTitle(title); 171 } 172 } 173 174 @Override 175 public boolean onOptionsItemSelected(MenuItem item) { 176 // This callback is used only when mSoloFragment == true (see onActivityCreated above) 177 switch (item.getItemId()) { 178 case android.R.id.home: 179 // App icon in Action Bar clicked; go up 180 Intent intent = new Intent(getActivity(), MainActivity.class); 181 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // Reuse the existing instance 182 startActivity(intent); 183 return true; 184 default: 185 return super.onOptionsItemSelected(item); 186 } 187 } 188 189 @Override 190 public void onSaveInstanceState (Bundle outState) { 191 super.onSaveInstanceState(outState); 192 outState.putInt("listPosition", mCurPosition); 193 outState.putInt("category", mCategory); 194 outState.putBoolean("systemUiVisible", mSystemUiVisible); 195 } 196 197 /** Toggle whether the system UI (status bar / system bar) is visible. 198 * This also toggles the action bar visibility. 199 * @param show True to show the system UI, false to hide it. 200 */ 201 void setSystemUiVisible(boolean show) { 202 mSystemUiVisible = show; 203 204 Window window = getActivity().getWindow(); 205 WindowManager.LayoutParams winParams = window.getAttributes(); 206 View view = getView(); 207 ActionBar actionBar = getActivity().getActionBar(); 208 209 if (show) { 210 // Show status bar (remove fullscreen flag) 211 window.setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN); 212 // Show system bar 213 view.setSystemUiVisibility(View.STATUS_BAR_VISIBLE); 214 // Show action bar 215 actionBar.show(); 216 } else { 217 // Add fullscreen flag (hide status bar) 218 window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 219 WindowManager.LayoutParams.FLAG_FULLSCREEN); 220 // Hide system bar 221 view.setSystemUiVisibility(View.STATUS_BAR_HIDDEN); 222 // Hide action bar 223 actionBar.hide(); 224 } 225 window.setAttributes(winParams); 226 } 227 228 boolean processDragStarted(DragEvent event) { 229 // Determine whether to continue processing drag and drop based on the 230 // plain text mime type. 231 ClipDescription clipDesc = event.getClipDescription(); 232 if (clipDesc != null) { 233 return clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN); 234 } 235 return false; 236 } 237 238 boolean processDrop(DragEvent event, ImageView imageView) { 239 // Attempt to parse clip data with expected format: category||entry_id. 240 // Ignore event if data does not conform to this format. 241 ClipData data = event.getClipData(); 242 if (data != null) { 243 if (data.getItemCount() > 0) { 244 Item item = data.getItemAt(0); 245 String textData = (String) item.getText(); 246 if (textData != null) { 247 StringTokenizer tokenizer = new StringTokenizer(textData, "||"); 248 if (tokenizer.countTokens() != 2) { 249 return false; 250 } 251 int category = -1; 252 int entryId = -1; 253 try { 254 category = Integer.parseInt(tokenizer.nextToken()); 255 entryId = Integer.parseInt(tokenizer.nextToken()); 256 } catch (NumberFormatException exception) { 257 return false; 258 } 259 updateContentAndRecycleBitmap(category, entryId); 260 // Update list fragment with selected entry. 261 TitlesFragment titlesFrag = (TitlesFragment) 262 getFragmentManager().findFragmentById(R.id.titles_frag); 263 titlesFrag.selectPosition(entryId); 264 return true; 265 } 266 } 267 } 268 return false; 269 } 270 271 /** 272 * Sets the current image visible. 273 * @param category Index position of the image category 274 * @param position Index position of the image 275 */ 276 void updateContentAndRecycleBitmap(int category, int position) { 277 mCategory = category; 278 mCurPosition = position; 279 280 if (mCurrentActionMode != null) { 281 mCurrentActionMode.finish(); 282 } 283 284 if (mBitmap != null) { 285 // This is an advanced call and should be used if you 286 // are working with a lot of bitmaps. The bitmap is dead 287 // after this call. 288 mBitmap.recycle(); 289 } 290 291 // Get the bitmap that needs to be drawn and update the ImageView 292 mBitmap = Directory.getCategory(category).getEntry(position) 293 .getBitmap(getResources()); 294 ((ImageView) getView().findViewById(R.id.image)).setImageBitmap(mBitmap); 295 } 296 297 /** Share the currently selected photo using an AsyncTask to compress the image 298 * and then invoke the appropriate share intent. 299 */ 300 void shareCurrentPhoto() { 301 File externalCacheDir = getActivity().getExternalCacheDir(); 302 if (externalCacheDir == null) { 303 Toast.makeText(getActivity(), "Error writing to USB/external storage.", 304 Toast.LENGTH_SHORT).show(); 305 return; 306 } 307 308 // Prevent media scanning of the cache directory. 309 final File noMediaFile = new File(externalCacheDir, ".nomedia"); 310 try { 311 noMediaFile.createNewFile(); 312 } catch (IOException e) { 313 } 314 315 // Write the bitmap to temporary storage in the external storage directory (e.g. SD card). 316 // We perform the actual disk write operations on a separate thread using the 317 // {@link AsyncTask} class, thus avoiding the possibility of stalling the main (UI) thread. 318 319 final File tempFile = new File(externalCacheDir, "tempfile.jpg"); 320 321 new AsyncTask<Void, Void, Boolean>() { 322 /** 323 * Compress and write the bitmap to disk on a separate thread. 324 * @return TRUE if the write was successful, FALSE otherwise. 325 */ 326 @Override 327 protected Boolean doInBackground(Void... voids) { 328 try { 329 FileOutputStream fo = new FileOutputStream(tempFile, false); 330 if (!mBitmap.compress(Bitmap.CompressFormat.JPEG, 60, fo)) { 331 Toast.makeText(getActivity(), "Error writing bitmap data.", 332 Toast.LENGTH_SHORT).show(); 333 return Boolean.FALSE; 334 } 335 return Boolean.TRUE; 336 337 } catch (FileNotFoundException e) { 338 Toast.makeText(getActivity(), "Error writing to USB/external storage.", 339 Toast.LENGTH_SHORT).show(); 340 return Boolean.FALSE; 341 } 342 } 343 344 /** 345 * After doInBackground completes (either successfully or in failure), we invoke an 346 * intent to share the photo. This code is run on the main (UI) thread. 347 */ 348 @Override 349 protected void onPostExecute(Boolean result) { 350 if (result != Boolean.TRUE) { 351 return; 352 } 353 354 Intent shareIntent = new Intent(Intent.ACTION_SEND); 355 shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(tempFile)); 356 shareIntent.setType("image/jpeg"); 357 startActivity(Intent.createChooser(shareIntent, "Share photo")); 358 } 359 }.execute(); 360 } 361 362 /** 363 * The callback for the 'photo selected' {@link ActionMode}. In this action mode, we can 364 * provide contextual actions for the selected photo. We currently only provide the 'share' 365 * action, but we could also add clipboard functions such as cut/copy/paste here as well. 366 */ 367 private ActionMode.Callback mContentSelectionActionModeCallback = new ActionMode.Callback() { 368 public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { 369 actionMode.setTitle(R.string.photo_selection_cab_title); 370 371 MenuInflater inflater = getActivity().getMenuInflater(); 372 inflater.inflate(R.menu.photo_context_menu, menu); 373 return true; 374 } 375 376 public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { 377 return false; 378 } 379 380 public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { 381 switch (menuItem.getItemId()) { 382 case R.id.menu_share: 383 shareCurrentPhoto(); 384 actionMode.finish(); 385 return true; 386 } 387 return false; 388 } 389 390 public void onDestroyActionMode(ActionMode actionMode) { 391 mContentView.setSelected(false); 392 mCurrentActionMode = null; 393 } 394 }; 395 } 396