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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onActivityCreated(Bundle savedInstanceState)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
onOptionsItemSelected(MenuItem item)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
onSaveInstanceState(Bundle outState)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      */
setSystemUiVisible(boolean show)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 
processDragStarted(DragEvent event)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 
processDrop(DragEvent event, ImageView imageView)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      */
updateContentAndRecycleBitmap(int category, int position)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      */
shareCurrentPhoto()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