| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * 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.example.android.hcgallery; |
| |
| import android.app.ActionBar; |
| import android.app.Fragment; |
| import android.content.ClipData; |
| import android.content.ClipData.Item; |
| import android.content.ClipDescription; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.graphics.Color; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.view.ActionMode; |
| import android.view.DragEvent; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.view.WindowManager; |
| import android.widget.ImageView; |
| import android.widget.Toast; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.StringTokenizer; |
| |
| /** Fragment that shows the content selected from the TitlesFragment. |
| * When running on a screen size smaller than "large", this fragment is hosted in |
| * ContentActivity. Otherwise, it appears side by side with the TitlesFragment |
| * in MainActivity. */ |
| public class ContentFragment extends Fragment { |
| private View mContentView; |
| private int mCategory = 0; |
| private int mCurPosition = 0; |
| private boolean mSystemUiVisible = true; |
| private boolean mSoloFragment = false; |
| |
| // The bitmap currently used by ImageView |
| private Bitmap mBitmap = null; |
| |
| // Current action mode (contextual action bar, a.k.a. CAB) |
| private ActionMode mCurrentActionMode; |
| |
| /** This is where we initialize the fragment's UI and attach some |
| * event listeners to UI components. |
| */ |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| mContentView = inflater.inflate(R.layout.content_welcome, null); |
| final ImageView imageView = (ImageView) mContentView.findViewById(R.id.image); |
| mContentView.setDrawingCacheEnabled(false); |
| |
| // Handle drag events when a list item is dragged into the view |
| mContentView.setOnDragListener(new View.OnDragListener() { |
| public boolean onDrag(View view, DragEvent event) { |
| switch (event.getAction()) { |
| case DragEvent.ACTION_DRAG_ENTERED: |
| view.setBackgroundColor( |
| getResources().getColor(R.color.drag_active_color)); |
| break; |
| |
| case DragEvent.ACTION_DRAG_EXITED: |
| view.setBackgroundColor(Color.TRANSPARENT); |
| break; |
| |
| case DragEvent.ACTION_DRAG_STARTED: |
| return processDragStarted(event); |
| |
| case DragEvent.ACTION_DROP: |
| view.setBackgroundColor(Color.TRANSPARENT); |
| return processDrop(event, imageView); |
| } |
| return false; |
| } |
| }); |
| |
| // Show/hide the system status bar when single-clicking a photo. |
| mContentView.setOnClickListener(new OnClickListener() { |
| public void onClick(View view) { |
| if (mCurrentActionMode != null) { |
| // If we're in an action mode, don't toggle the action bar |
| return; |
| } |
| |
| if (mSystemUiVisible) { |
| setSystemUiVisible(false); |
| } else { |
| setSystemUiVisible(true); |
| } |
| } |
| }); |
| |
| // When long-pressing a photo, activate the action mode for selection, showing the |
| // contextual action bar (CAB). |
| mContentView.setOnLongClickListener(new View.OnLongClickListener() { |
| public boolean onLongClick(View view) { |
| if (mCurrentActionMode != null) { |
| return false; |
| } |
| |
| mCurrentActionMode = getActivity().startActionMode( |
| mContentSelectionActionModeCallback); |
| view.setSelected(true); |
| return true; |
| } |
| }); |
| |
| return mContentView; |
| } |
| |
| /** This is where we perform additional setup for the fragment that's either |
| * not related to the fragment's layout or must be done after the layout is drawn. |
| */ |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| |
| // Set member variable for whether this fragment is the only one in the activity |
| Fragment listFragment = getFragmentManager().findFragmentById(R.id.titles_frag); |
| mSoloFragment = listFragment == null ? true : false; |
| |
| if (mSoloFragment) { |
| // The fragment is alone, so enable up navigation |
| getActivity().getActionBar().setDisplayHomeAsUpEnabled(true); |
| // Must call in order to get callback to onOptionsItemSelected() |
| setHasOptionsMenu(true); |
| } |
| |
| // Current position and UI visibility should survive screen rotations. |
| if (savedInstanceState != null) { |
| setSystemUiVisible(savedInstanceState.getBoolean("systemUiVisible")); |
| if (mSoloFragment) { |
| // Restoring these members is not necessary when this fragment |
| // is combined with the TitlesFragment, because when the TitlesFragment |
| // is restored, it selects the appropriate item and sends the event |
| // to the updateContentAndRecycleBitmap() method itself |
| mCategory = savedInstanceState.getInt("category"); |
| mCurPosition = savedInstanceState.getInt("listPosition"); |
| updateContentAndRecycleBitmap(mCategory, mCurPosition); |
| } |
| } |
| |
| if (mSoloFragment) { |
| String title = Directory.getCategory(mCategory).getEntry(mCurPosition).getName(); |
| ActionBar bar = getActivity().getActionBar(); |
| bar.setTitle(title); |
| } |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| // This callback is used only when mSoloFragment == true (see onActivityCreated above) |
| switch (item.getItemId()) { |
| case android.R.id.home: |
| // App icon in Action Bar clicked; go up |
| Intent intent = new Intent(getActivity(), MainActivity.class); |
| intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // Reuse the existing instance |
| startActivity(intent); |
| return true; |
| default: |
| return super.onOptionsItemSelected(item); |
| } |
| } |
| |
| @Override |
| public void onSaveInstanceState (Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putInt("listPosition", mCurPosition); |
| outState.putInt("category", mCategory); |
| outState.putBoolean("systemUiVisible", mSystemUiVisible); |
| } |
| |
| /** Toggle whether the system UI (status bar / system bar) is visible. |
| * This also toggles the action bar visibility. |
| * @param show True to show the system UI, false to hide it. |
| */ |
| void setSystemUiVisible(boolean show) { |
| mSystemUiVisible = show; |
| |
| Window window = getActivity().getWindow(); |
| WindowManager.LayoutParams winParams = window.getAttributes(); |
| View view = getView(); |
| ActionBar actionBar = getActivity().getActionBar(); |
| |
| if (show) { |
| // Show status bar (remove fullscreen flag) |
| window.setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN); |
| // Show system bar |
| view.setSystemUiVisibility(View.STATUS_BAR_VISIBLE); |
| // Show action bar |
| actionBar.show(); |
| } else { |
| // Add fullscreen flag (hide status bar) |
| window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, |
| WindowManager.LayoutParams.FLAG_FULLSCREEN); |
| // Hide system bar |
| view.setSystemUiVisibility(View.STATUS_BAR_HIDDEN); |
| // Hide action bar |
| actionBar.hide(); |
| } |
| window.setAttributes(winParams); |
| } |
| |
| boolean processDragStarted(DragEvent event) { |
| // Determine whether to continue processing drag and drop based on the |
| // plain text mime type. |
| ClipDescription clipDesc = event.getClipDescription(); |
| if (clipDesc != null) { |
| return clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN); |
| } |
| return false; |
| } |
| |
| boolean processDrop(DragEvent event, ImageView imageView) { |
| // Attempt to parse clip data with expected format: category||entry_id. |
| // Ignore event if data does not conform to this format. |
| ClipData data = event.getClipData(); |
| if (data != null) { |
| if (data.getItemCount() > 0) { |
| Item item = data.getItemAt(0); |
| String textData = (String) item.getText(); |
| if (textData != null) { |
| StringTokenizer tokenizer = new StringTokenizer(textData, "||"); |
| if (tokenizer.countTokens() != 2) { |
| return false; |
| } |
| int category = -1; |
| int entryId = -1; |
| try { |
| category = Integer.parseInt(tokenizer.nextToken()); |
| entryId = Integer.parseInt(tokenizer.nextToken()); |
| } catch (NumberFormatException exception) { |
| return false; |
| } |
| updateContentAndRecycleBitmap(category, entryId); |
| // Update list fragment with selected entry. |
| TitlesFragment titlesFrag = (TitlesFragment) |
| getFragmentManager().findFragmentById(R.id.titles_frag); |
| titlesFrag.selectPosition(entryId); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Sets the current image visible. |
| * @param category Index position of the image category |
| * @param position Index position of the image |
| */ |
| void updateContentAndRecycleBitmap(int category, int position) { |
| mCategory = category; |
| mCurPosition = position; |
| |
| if (mCurrentActionMode != null) { |
| mCurrentActionMode.finish(); |
| } |
| |
| if (mBitmap != null) { |
| // This is an advanced call and should be used if you |
| // are working with a lot of bitmaps. The bitmap is dead |
| // after this call. |
| mBitmap.recycle(); |
| } |
| |
| // Get the bitmap that needs to be drawn and update the ImageView |
| mBitmap = Directory.getCategory(category).getEntry(position) |
| .getBitmap(getResources()); |
| ((ImageView) getView().findViewById(R.id.image)).setImageBitmap(mBitmap); |
| } |
| |
| /** Share the currently selected photo using an AsyncTask to compress the image |
| * and then invoke the appropriate share intent. |
| */ |
| void shareCurrentPhoto() { |
| File externalCacheDir = getActivity().getExternalCacheDir(); |
| if (externalCacheDir == null) { |
| Toast.makeText(getActivity(), "Error writing to USB/external storage.", |
| Toast.LENGTH_SHORT).show(); |
| return; |
| } |
| |
| // Prevent media scanning of the cache directory. |
| final File noMediaFile = new File(externalCacheDir, ".nomedia"); |
| try { |
| noMediaFile.createNewFile(); |
| } catch (IOException e) { |
| } |
| |
| // Write the bitmap to temporary storage in the external storage directory (e.g. SD card). |
| // We perform the actual disk write operations on a separate thread using the |
| // {@link AsyncTask} class, thus avoiding the possibility of stalling the main (UI) thread. |
| |
| final File tempFile = new File(externalCacheDir, "tempfile.jpg"); |
| |
| new AsyncTask<Void, Void, Boolean>() { |
| /** |
| * Compress and write the bitmap to disk on a separate thread. |
| * @return TRUE if the write was successful, FALSE otherwise. |
| */ |
| @Override |
| protected Boolean doInBackground(Void... voids) { |
| try { |
| FileOutputStream fo = new FileOutputStream(tempFile, false); |
| if (!mBitmap.compress(Bitmap.CompressFormat.JPEG, 60, fo)) { |
| Toast.makeText(getActivity(), "Error writing bitmap data.", |
| Toast.LENGTH_SHORT).show(); |
| return Boolean.FALSE; |
| } |
| return Boolean.TRUE; |
| |
| } catch (FileNotFoundException e) { |
| Toast.makeText(getActivity(), "Error writing to USB/external storage.", |
| Toast.LENGTH_SHORT).show(); |
| return Boolean.FALSE; |
| } |
| } |
| |
| /** |
| * After doInBackground completes (either successfully or in failure), we invoke an |
| * intent to share the photo. This code is run on the main (UI) thread. |
| */ |
| @Override |
| protected void onPostExecute(Boolean result) { |
| if (result != Boolean.TRUE) { |
| return; |
| } |
| |
| Intent shareIntent = new Intent(Intent.ACTION_SEND); |
| shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(tempFile)); |
| shareIntent.setType("image/jpeg"); |
| startActivity(Intent.createChooser(shareIntent, "Share photo")); |
| } |
| }.execute(); |
| } |
| |
| /** |
| * The callback for the 'photo selected' {@link ActionMode}. In this action mode, we can |
| * provide contextual actions for the selected photo. We currently only provide the 'share' |
| * action, but we could also add clipboard functions such as cut/copy/paste here as well. |
| */ |
| private ActionMode.Callback mContentSelectionActionModeCallback = new ActionMode.Callback() { |
| public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { |
| actionMode.setTitle(R.string.photo_selection_cab_title); |
| |
| MenuInflater inflater = getActivity().getMenuInflater(); |
| inflater.inflate(R.menu.photo_context_menu, menu); |
| return true; |
| } |
| |
| public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { |
| return false; |
| } |
| |
| public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { |
| switch (menuItem.getItemId()) { |
| case R.id.menu_share: |
| shareCurrentPhoto(); |
| actionMode.finish(); |
| return true; |
| } |
| return false; |
| } |
| |
| public void onDestroyActionMode(ActionMode actionMode) { |
| mContentView.setSelected(false); |
| mCurrentActionMode = null; |
| } |
| }; |
| } |