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