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