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