Home | History | Annotate | Download | only in callcomposer
      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 
     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