1 /*
2  * Copyright (C) 2016 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.android.dialer.callcomposer;
18 
19 import static android.app.Activity.RESULT_OK;
20 
21 import android.Manifest.permission;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.database.Cursor;
25 import android.net.Uri;
26 import android.os.Bundle;
27 import android.os.Parcelable;
28 import android.provider.Settings;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.Nullable;
31 import android.support.v4.app.LoaderManager.LoaderCallbacks;
32 import android.support.v4.content.CursorLoader;
33 import android.support.v4.content.Loader;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.View.OnClickListener;
37 import android.view.ViewGroup;
38 import android.widget.GridView;
39 import android.widget.ImageView;
40 import android.widget.TextView;
41 import com.android.dialer.common.LogUtil;
42 import com.android.dialer.common.concurrent.DialerExecutor;
43 import com.android.dialer.common.concurrent.DialerExecutorComponent;
44 import com.android.dialer.logging.DialerImpression;
45 import com.android.dialer.logging.Logger;
46 import com.android.dialer.theme.base.ThemeComponent;
47 import com.android.dialer.util.PermissionsUtil;
48 import java.util.ArrayList;
49 import java.util.List;
50 
51 /** Fragment used to compose call with image from the user's gallery. */
52 public class GalleryComposerFragment extends CallComposerFragment
53     implements LoaderCallbacks<Cursor>, OnClickListener {
54 
55   private static final String SELECTED_DATA_KEY = "selected_data";
56   private static final String IS_COPY_KEY = "is_copy";
57   private static final String INSERTED_IMAGES_KEY = "inserted_images";
58 
59   private static final int RESULT_LOAD_IMAGE = 1;
60   private static final int RESULT_OPEN_SETTINGS = 2;
61 
62   private GalleryGridAdapter adapter;
63   private GridView galleryGridView;
64   private View permissionView;
65   private View allowPermission;
66 
67   private String[] permissions = new String[] {permission.READ_EXTERNAL_STORAGE};
68   private CursorLoader cursorLoader;
69   private GalleryGridItemData selectedData = null;
70   private boolean selectedDataIsCopy;
71   private List<GalleryGridItemData> insertedImages = new ArrayList<>();
72 
73   private DialerExecutor<Uri> copyAndResizeImage;
74 
newInstance()75   public static GalleryComposerFragment newInstance() {
76     return new GalleryComposerFragment();
77   }
78 
79   @Nullable
80   @Override
onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle)81   public View onCreateView(
82       LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) {
83     View view = inflater.inflate(R.layout.fragment_gallery_composer, container, false);
84     galleryGridView = (GridView) view.findViewById(R.id.gallery_grid_view);
85     permissionView = view.findViewById(R.id.permission_view);
86 
87     if (!PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) {
88       Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DISPLAYED);
89       LogUtil.i("GalleryComposerFragment.onCreateView", "Permission view shown.");
90       ImageView permissionImage = (ImageView) permissionView.findViewById(R.id.permission_icon);
91       TextView permissionText = (TextView) permissionView.findViewById(R.id.permission_text);
92       allowPermission = permissionView.findViewById(R.id.allow);
93 
94       allowPermission.setOnClickListener(this);
95       permissionText.setText(R.string.gallery_permission_text);
96       permissionImage.setImageResource(R.drawable.quantum_ic_photo_white_48);
97       permissionImage.setColorFilter(ThemeComponent.get(getContext()).theme().getColorPrimary());
98       permissionView.setVisibility(View.VISIBLE);
99     } else {
100       if (bundle != null) {
101         selectedData = bundle.getParcelable(SELECTED_DATA_KEY);
102         selectedDataIsCopy = bundle.getBoolean(IS_COPY_KEY);
103         insertedImages = bundle.getParcelableArrayList(INSERTED_IMAGES_KEY);
104       }
105       setupGallery();
106     }
107     return view;
108   }
109 
110   @Override
onActivityCreated(@ullable Bundle bundle)111   public void onActivityCreated(@Nullable Bundle bundle) {
112     super.onActivityCreated(bundle);
113 
114     copyAndResizeImage =
115         DialerExecutorComponent.get(getContext())
116             .dialerExecutorFactory()
117             .createUiTaskBuilder(
118                 getActivity().getFragmentManager(),
119                 "copyAndResizeImage",
120                 new CopyAndResizeImageWorker(getActivity().getApplicationContext()))
121             .onSuccess(
122                 output -> {
123                   GalleryGridItemData data1 =
124                       adapter.insertEntry(output.first.getAbsolutePath(), output.second);
125                   insertedImages.add(0, data1);
126                   setSelected(data1, true);
127                 })
128             .onFailure(
129                 throwable -> {
130                   // TODO(a bug) - gracefully handle message failure
131                   LogUtil.e(
132                       "GalleryComposerFragment.onFailure", "data preparation failed", throwable);
133                 })
134             .build();
135   }
136 
setupGallery()137   private void setupGallery() {
138     adapter = new GalleryGridAdapter(getContext(), null, this);
139     galleryGridView.setAdapter(adapter);
140     getLoaderManager().initLoader(0 /* id */, null /* args */, this /* loaderCallbacks */);
141   }
142 
143   @Override
onCreateLoader(int id, Bundle args)144   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
145     return cursorLoader = new GalleryCursorLoader(getContext());
146   }
147 
148   @Override
onLoadFinished(Loader<Cursor> loader, Cursor cursor)149   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
150     adapter.swapCursor(cursor);
151     if (insertedImages != null && !insertedImages.isEmpty()) {
152       adapter.insertEntries(insertedImages);
153     }
154     setSelected(selectedData, selectedDataIsCopy);
155   }
156 
157   @Override
onLoaderReset(Loader<Cursor> loader)158   public void onLoaderReset(Loader<Cursor> loader) {
159     adapter.swapCursor(null);
160   }
161 
162   @Override
onClick(View view)163   public void onClick(View view) {
164     if (view == allowPermission) {
165       // Checks to see if the user has permanently denied this permission. If this is their first
166       // time seeing this permission or they've only pressed deny previously, they will see the
167       // permission request. If they've permanently denied the permission, they will be sent to
168       // Dialer settings in order to enable the permission.
169       if (PermissionsUtil.isFirstRequest(getContext(), permissions[0])
170           || shouldShowRequestPermissionRationale(permissions[0])) {
171         LogUtil.i("GalleryComposerFragment.onClick", "Storage permission requested.");
172         Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_REQUESTED);
173         requestPermissions(permissions, STORAGE_PERMISSION);
174       } else {
175         LogUtil.i("GalleryComposerFragment.onClick", "Settings opened to enable permission.");
176         Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_SETTINGS);
177         Intent intent = new Intent(Intent.ACTION_VIEW);
178         intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
179         intent.setData(Uri.parse("package:" + getContext().getPackageName()));
180         startActivityForResult(intent, RESULT_OPEN_SETTINGS);
181       }
182       return;
183     } else {
184       GalleryGridItemView itemView = ((GalleryGridItemView) view);
185       if (itemView.isGallery()) {
186         Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
187         intent.setType("image/*");
188         intent.putExtra(Intent.EXTRA_MIME_TYPES, GalleryCursorLoader.ACCEPTABLE_IMAGE_TYPES);
189         intent.addCategory(Intent.CATEGORY_OPENABLE);
190         startActivityForResult(intent, RESULT_LOAD_IMAGE);
191       } else if (itemView.getData().equals(selectedData)) {
192         clearComposer();
193       } else {
194         setSelected(new GalleryGridItemData(itemView.getData()), false);
195       }
196     }
197   }
198 
199   @Nullable
getGalleryData()200   public GalleryGridItemData getGalleryData() {
201     return selectedData;
202   }
203 
getGalleryGridView()204   public GridView getGalleryGridView() {
205     return galleryGridView;
206   }
207 
208   @Override
onActivityResult(int requestCode, int resultCode, Intent data)209   public void onActivityResult(int requestCode, int resultCode, Intent data) {
210     super.onActivityResult(requestCode, resultCode, data);
211     if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && data != null) {
212       prepareDataForAttachment(data);
213     } else if (requestCode == RESULT_OPEN_SETTINGS
214         && PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) {
215       permissionView.setVisibility(View.GONE);
216       setupGallery();
217     }
218   }
219 
setSelected(GalleryGridItemData data, boolean isCopy)220   private void setSelected(GalleryGridItemData data, boolean isCopy) {
221     selectedData = data;
222     selectedDataIsCopy = isCopy;
223     adapter.setSelected(selectedData);
224     CallComposerListener listener = getListener();
225     if (listener != null) {
226       getListener().composeCall(this);
227     }
228   }
229 
230   @Override
shouldHide()231   public boolean shouldHide() {
232     return selectedData == null
233         || selectedData.getFilePath() == null
234         || selectedData.getMimeType() == null;
235   }
236 
237   @Override
clearComposer()238   public void clearComposer() {
239     setSelected(null, false);
240   }
241 
242   @Override
onSaveInstanceState(Bundle outState)243   public void onSaveInstanceState(Bundle outState) {
244     super.onSaveInstanceState(outState);
245     outState.putParcelable(SELECTED_DATA_KEY, selectedData);
246     outState.putBoolean(IS_COPY_KEY, selectedDataIsCopy);
247     outState.putParcelableArrayList(
248         INSERTED_IMAGES_KEY, (ArrayList<? extends Parcelable>) insertedImages);
249   }
250 
251   @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)252   public void onRequestPermissionsResult(
253       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
254     if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
255       PermissionsUtil.permissionRequested(getContext(), permissions[0]);
256     }
257     if (requestCode == STORAGE_PERMISSION
258         && grantResults.length > 0
259         && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
260       Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_GRANTED);
261       LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission granted.");
262       permissionView.setVisibility(View.GONE);
263       setupGallery();
264     } else if (requestCode == STORAGE_PERMISSION) {
265       Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DENIED);
266       LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission denied.");
267     }
268   }
269 
getCursorLoader()270   public CursorLoader getCursorLoader() {
271     return cursorLoader;
272   }
273 
selectedDataIsCopy()274   public boolean selectedDataIsCopy() {
275     return selectedDataIsCopy;
276   }
277 
prepareDataForAttachment(Intent data)278   private void prepareDataForAttachment(Intent data) {
279     // We're using the builtin photo picker which supplies the return url as it's "data".
280     String url = data.getDataString();
281     if (url == null) {
282       final Bundle extras = data.getExtras();
283       if (extras != null) {
284         final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
285         if (uri != null) {
286           url = uri.toString();
287         }
288       }
289     }
290 
291     // This should never happen, but just in case..
292     // Guard against null uri cases for when the activity returns a null/invalid intent.
293     if (url != null) {
294       copyAndResizeImage.executeParallel(Uri.parse(url));
295     } else {
296       // TODO(a bug) - gracefully handle message failure
297     }
298   }
299 }
300