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