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
17package com.android.dialer.callcomposer;
18
19import static android.app.Activity.RESULT_OK;
20
21import android.Manifest.permission;
22import android.content.Intent;
23import android.content.pm.PackageManager;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.Parcelable;
28import android.provider.Settings;
29import android.support.annotation.NonNull;
30import android.support.annotation.Nullable;
31import android.support.annotation.VisibleForTesting;
32import android.support.v4.app.LoaderManager.LoaderCallbacks;
33import android.support.v4.content.ContextCompat;
34import android.support.v4.content.CursorLoader;
35import android.support.v4.content.Loader;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.View.OnClickListener;
39import android.view.ViewGroup;
40import android.widget.GridView;
41import android.widget.ImageView;
42import android.widget.TextView;
43import com.android.dialer.common.Assert;
44import com.android.dialer.common.LogUtil;
45import com.android.dialer.common.concurrent.DefaultDialerExecutorFactory;
46import com.android.dialer.common.concurrent.DialerExecutor;
47import com.android.dialer.common.concurrent.DialerExecutorFactory;
48import com.android.dialer.logging.DialerImpression;
49import com.android.dialer.logging.Logger;
50import com.android.dialer.util.PermissionsUtil;
51import java.util.ArrayList;
52import java.util.List;
53
54/** Fragment used to compose call with image from the user's gallery. */
55public 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
80  public static GalleryComposerFragment newInstance() {
81    return new GalleryComposerFragment();
82  }
83
84  @VisibleForTesting
85  void setExecutorFactory(@NonNull DialerExecutorFactory executorFactory) {
86    this.executorFactory = Assert.isNotNull(executorFactory);
87  }
88
89  @Nullable
90  @Override
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
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
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
154  public Loader<Cursor> onCreateLoader(int id, Bundle args) {
155    return cursorLoader = new GalleryCursorLoader(getContext());
156  }
157
158  @Override
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
168  public void onLoaderReset(Loader<Cursor> loader) {
169    adapter.swapCursor(null);
170  }
171
172  @Override
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
210  public GalleryGridItemData getGalleryData() {
211    return selectedData;
212  }
213
214  public GridView getGalleryGridView() {
215    return galleryGridView;
216  }
217
218  @Override
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
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
241  public boolean shouldHide() {
242    return selectedData == null
243        || selectedData.getFilePath() == null
244        || selectedData.getMimeType() == null;
245  }
246
247  @Override
248  public void clearComposer() {
249    setSelected(null, false);
250  }
251
252  @Override
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
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
280  public CursorLoader getCursorLoader() {
281    return cursorLoader;
282  }
283
284  public boolean selectedDataIsCopy() {
285    return selectedDataIsCopy;
286  }
287
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