/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dialer.callcomposer; import static android.app.Activity.RESULT_OK; import android.Manifest.permission; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.GridView; import android.widget.ImageView; import android.widget.TextView; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.theme.base.ThemeComponent; import com.android.dialer.util.PermissionsUtil; import java.util.ArrayList; import java.util.List; /** Fragment used to compose call with image from the user's gallery. */ public class GalleryComposerFragment extends CallComposerFragment implements LoaderCallbacks, OnClickListener { private static final String SELECTED_DATA_KEY = "selected_data"; private static final String IS_COPY_KEY = "is_copy"; private static final String INSERTED_IMAGES_KEY = "inserted_images"; private static final int RESULT_LOAD_IMAGE = 1; private static final int RESULT_OPEN_SETTINGS = 2; private GalleryGridAdapter adapter; private GridView galleryGridView; private View permissionView; private View allowPermission; private String[] permissions = new String[] {permission.READ_EXTERNAL_STORAGE}; private CursorLoader cursorLoader; private GalleryGridItemData selectedData = null; private boolean selectedDataIsCopy; private List insertedImages = new ArrayList<>(); private DialerExecutor copyAndResizeImage; public static GalleryComposerFragment newInstance() { return new GalleryComposerFragment(); } @Nullable @Override public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) { View view = inflater.inflate(R.layout.fragment_gallery_composer, container, false); galleryGridView = (GridView) view.findViewById(R.id.gallery_grid_view); permissionView = view.findViewById(R.id.permission_view); if (!PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) { Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DISPLAYED); LogUtil.i("GalleryComposerFragment.onCreateView", "Permission view shown."); ImageView permissionImage = (ImageView) permissionView.findViewById(R.id.permission_icon); TextView permissionText = (TextView) permissionView.findViewById(R.id.permission_text); allowPermission = permissionView.findViewById(R.id.allow); allowPermission.setOnClickListener(this); permissionText.setText(R.string.gallery_permission_text); permissionImage.setImageResource(R.drawable.quantum_ic_photo_white_48); permissionImage.setColorFilter(ThemeComponent.get(getContext()).theme().getColorPrimary()); permissionView.setVisibility(View.VISIBLE); } else { if (bundle != null) { selectedData = bundle.getParcelable(SELECTED_DATA_KEY); selectedDataIsCopy = bundle.getBoolean(IS_COPY_KEY); insertedImages = bundle.getParcelableArrayList(INSERTED_IMAGES_KEY); } setupGallery(); } return view; } @Override public void onActivityCreated(@Nullable Bundle bundle) { super.onActivityCreated(bundle); copyAndResizeImage = DialerExecutorComponent.get(getContext()) .dialerExecutorFactory() .createUiTaskBuilder( getActivity().getFragmentManager(), "copyAndResizeImage", new CopyAndResizeImageWorker(getActivity().getApplicationContext())) .onSuccess( output -> { GalleryGridItemData data1 = adapter.insertEntry(output.first.getAbsolutePath(), output.second); insertedImages.add(0, data1); setSelected(data1, true); }) .onFailure( throwable -> { // TODO(a bug) - gracefully handle message failure LogUtil.e( "GalleryComposerFragment.onFailure", "data preparation failed", throwable); }) .build(); } private void setupGallery() { adapter = new GalleryGridAdapter(getContext(), null, this); galleryGridView.setAdapter(adapter); getLoaderManager().initLoader(0 /* id */, null /* args */, this /* loaderCallbacks */); } @Override public Loader onCreateLoader(int id, Bundle args) { return cursorLoader = new GalleryCursorLoader(getContext()); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { adapter.swapCursor(cursor); if (insertedImages != null && !insertedImages.isEmpty()) { adapter.insertEntries(insertedImages); } setSelected(selectedData, selectedDataIsCopy); } @Override public void onLoaderReset(Loader loader) { adapter.swapCursor(null); } @Override public void onClick(View view) { if (view == allowPermission) { // Checks to see if the user has permanently denied this permission. If this is their first // time seeing this permission or they've only pressed deny previously, they will see the // permission request. If they've permanently denied the permission, they will be sent to // Dialer settings in order to enable the permission. if (PermissionsUtil.isFirstRequest(getContext(), permissions[0]) || shouldShowRequestPermissionRationale(permissions[0])) { LogUtil.i("GalleryComposerFragment.onClick", "Storage permission requested."); Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_REQUESTED); requestPermissions(permissions, STORAGE_PERMISSION); } else { LogUtil.i("GalleryComposerFragment.onClick", "Settings opened to enable permission."); Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_SETTINGS); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + getContext().getPackageName())); startActivityForResult(intent, RESULT_OPEN_SETTINGS); } return; } else { GalleryGridItemView itemView = ((GalleryGridItemView) view); if (itemView.isGallery()) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); intent.putExtra(Intent.EXTRA_MIME_TYPES, GalleryCursorLoader.ACCEPTABLE_IMAGE_TYPES); intent.addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(intent, RESULT_LOAD_IMAGE); } else if (itemView.getData().equals(selectedData)) { clearComposer(); } else { setSelected(new GalleryGridItemData(itemView.getData()), false); } } } @Nullable public GalleryGridItemData getGalleryData() { return selectedData; } public GridView getGalleryGridView() { return galleryGridView; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && data != null) { prepareDataForAttachment(data); } else if (requestCode == RESULT_OPEN_SETTINGS && PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) { permissionView.setVisibility(View.GONE); setupGallery(); } } private void setSelected(GalleryGridItemData data, boolean isCopy) { selectedData = data; selectedDataIsCopy = isCopy; adapter.setSelected(selectedData); CallComposerListener listener = getListener(); if (listener != null) { getListener().composeCall(this); } } @Override public boolean shouldHide() { return selectedData == null || selectedData.getFilePath() == null || selectedData.getMimeType() == null; } @Override public void clearComposer() { setSelected(null, false); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_DATA_KEY, selectedData); outState.putBoolean(IS_COPY_KEY, selectedDataIsCopy); outState.putParcelableArrayList( INSERTED_IMAGES_KEY, (ArrayList) insertedImages); } @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) { PermissionsUtil.permissionRequested(getContext(), permissions[0]); } if (requestCode == STORAGE_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_GRANTED); LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission granted."); permissionView.setVisibility(View.GONE); setupGallery(); } else if (requestCode == STORAGE_PERMISSION) { Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DENIED); LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission denied."); } } public CursorLoader getCursorLoader() { return cursorLoader; } public boolean selectedDataIsCopy() { return selectedDataIsCopy; } private void prepareDataForAttachment(Intent data) { // We're using the builtin photo picker which supplies the return url as it's "data". String url = data.getDataString(); if (url == null) { final Bundle extras = data.getExtras(); if (extras != null) { final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM); if (uri != null) { url = uri.toString(); } } } // This should never happen, but just in case.. // Guard against null uri cases for when the activity returns a null/invalid intent. if (url != null) { copyAndResizeImage.executeParallel(Uri.parse(url)); } else { // TODO(a bug) - gracefully handle message failure } } }