Skip to content
This repository was archived by the owner on Jul 25, 2024. It is now read-only.

Commit 289f7ab

Browse files
Sam1301timabbott
authored andcommitted
Add photo uploads, cropping and marking tools.
* Set up camera button. Launch MediaStore.ACTION_IMAGE_CAPTURE intent on clicking camera button. * Set up file provider to get full sized image. The path for images directory in the app will be different for debug and realese builds. In order to use getExternalStorageDir() we need to ask write permission for android 4.3 and below. * Disable camera button while sending message. * Add PhotoSendActivity to crop captured photo. Android-image-cropper library is used for cropping functionality. * Use Glide library to decode bitmaps with high performance. * Set up delete, crop and send button in PhotoSendActivity. * Set up edit button in PhotoSendActivity to launch PhotoEditAcitvity. * Set up layout for PhotoEditActivity. * Add DrawCustomView.java for drawing on canvas. If same file path is used, the image is cached based on file path by Glide in PhotoEditActivity. So if the user changes the image in SendActivity (after pressing back from EditActivity), the changes are not reflected back in EditActivity. After glide loads a bitmap in an imageview, it uses its own drawable GlideBitmapDrawable to place inside the imageView unless we explicitly use .asBitmap() option. Hence, instanceOf should to used to distinguish between the two types. * Correct file paths in PhotoHelper.saveBitmapAsFile(). * Add colors and undo, back, send and crop button. This adds more color options to marker tool and sets up undo, back, send and crop buttons in PhotoEditActivity. * Load image when PhotoSendActivity is in focus. * Add cancel button and override back button function. These changes are committed in PhotoSendActivity. The cancel button takes user back to ZulipActivity. In case user has cropped the image, on pressing back these changes should be undone otherwise user should be sent back to ZulipActivity. * Activate crop button after redirection from PhotoEditActivity. During crop, after user is redirected to PhotoSendActivity from PhotoEditActivity, the crop button should appear in an active state. Fixes #228.
1 parent 20ba2bc commit 289f7ab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1221
-12
lines changed

app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ dependencies {
8383
compile 'com.squareup.picasso:picasso:2.5.2'
8484
//noinspection GradleCompatible
8585
compile 'com.android.support:customtabs:23.3.0'
86+
compile 'com.github.bumptech.glide:glide:3.7.0'
87+
compile 'com.android.support:support-v4:24.1.0'
88+
compile 'com.theartofdev.edmodo:android-image-cropper:2.3.+'
8689
compile('com.crashlytics.sdk.android:crashlytics:2.6.2@aar') {
8790
transitive = true;
8891
}

app/src/debug/res/xml/file_paths.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<paths>
3+
<external-path name="images" path="Android/data/com.zulip.android.dev/files/Pictures" />
4+
</paths>

app/src/main/AndroidManifest.xml

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,59 @@
77
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
88
<uses-permission android:name="android.permission.WAKE_LOCK" />
99
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
10-
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
10+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
11+
<uses-permission
12+
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
13+
android:maxSdkVersion="18" />
14+
15+
<uses-feature
16+
android:name="android.hardware.camera"
17+
android:required="false" />
1118

1219
<permission
1320
android:name="${applicationId}.permission.C2D_MESSAGE"
1421
android:protectionLevel="signature" />
1522

1623
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
17-
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
18-
<uses-permission android:name="android.permission.VIBRATE"/>
24+
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
25+
<uses-permission android:name="android.permission.VIBRATE" />
1926

2027
<uses-sdk android:targetSdkVersion="23" android:minSdkVersion="13"
2128
tools:overrideLibrary="com.android.support:customtabs:23.3.0,android.support.customtabs"/>
2229

2330
<application
24-
android:name="com.zulip.android.ZulipApp"
31+
android:name=".ZulipApp"
2532
android:allowBackup="false"
2633
android:icon="@drawable/ic_launcher"
2734
android:label="@string/app_name"
2835
android:theme="@style/AppTheme.DayNight.NoActionBar"
2936
android:supportsRtl="false"
3037
tools:ignore="GoogleAppIndexingWarning">
38+
<provider
39+
android:name="android.support.v4.content.FileProvider"
40+
android:authorities="com.zulip.fileprovider"
41+
android:exported="false"
42+
android:grantUriPermissions="true">
43+
<meta-data
44+
android:name="android.support.FILE_PROVIDER_PATHS"
45+
android:resource="@xml/file_paths" />
46+
</provider>
47+
3148
<activity
3249
android:name=".activities.ZulipActivity"
3350
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
3451
android:label="@string/app_name"
35-
android:launchMode="singleTask" >
52+
android:launchMode="singleTask">
3653
<intent-filter>
3754
<action android:name="android.intent.action.MAIN" />
3855

3956
<category android:name="android.intent.category.LAUNCHER" />
4057
</intent-filter>
4158
<intent-filter>
4259
<action android:name="android.intent.action.SEND" />
60+
4361
<category android:name="android.intent.category.DEFAULT" />
62+
4463
<data android:mimeType="image/*" />
4564
</intent-filter>
4665
</activity>
@@ -50,12 +69,11 @@
5069
</activity>
5170
<activity
5271
android:name=".activities.LegalActivity"
53-
android:label="@string/title_activity_legal" >
54-
</activity>
72+
android:label="@string/title_activity_legal" />
5573

5674
<receiver
5775
android:name=".gcm.GcmBroadcastReceiver"
58-
android:permission="com.google.android.c2dm.permission.SEND" >
76+
android:permission="com.google.android.c2dm.permission.SEND">
5977
<intent-filter>
6078
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
6179

@@ -64,7 +82,7 @@
6482
</receiver>
6583
<receiver
6684
android:name=".gcm.GcmShowNotificationReceiver"
67-
android:exported="false" >
85+
android:exported="false">
6886
<intent-filter>
6987
<action android:name="${applicationId}.PushMessage.BROADCAST" />
7088
</intent-filter>
@@ -77,9 +95,11 @@
7795
android:value="@integer/google_play_services_version" />
7896

7997
<activity android:name=".activities.DevAuthActivity" />
98+
8099
<meta-data
81100
android:name="io.fabric.ApiKey"
82101
android:value="${fabricKey}" />
102+
83103
<receiver android:name=".widget.ZulipWidget">
84104
<intent-filter>
85105
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -99,7 +119,8 @@
99119
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
100120
</intent-filter>
101121
</activity>
102-
122+
<activity android:name=".activities.PhotoSendActivity" />
123+
<activity android:name=".activities.PhotoEditActivity"></activity>
103124
</application>
104125

105-
</manifest>
126+
</manifest>
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package com.zulip.android.activities;
2+
3+
import android.content.Intent;
4+
import android.graphics.Bitmap;
5+
import android.graphics.Canvas;
6+
import android.graphics.Color;
7+
import android.graphics.Point;
8+
import android.graphics.drawable.Drawable;
9+
import android.graphics.drawable.GradientDrawable;
10+
import android.os.Bundle;
11+
import android.support.v4.content.ContextCompat;
12+
import android.support.v7.app.AppCompatActivity;
13+
import android.util.Log;
14+
import android.view.Display;
15+
import android.view.View;
16+
import android.view.WindowManager;
17+
import android.widget.FrameLayout;
18+
import android.widget.ImageView;
19+
import android.widget.TextView;
20+
21+
import com.bumptech.glide.Glide;
22+
import com.bumptech.glide.request.animation.GlideAnimation;
23+
import com.bumptech.glide.request.target.SimpleTarget;
24+
import com.zulip.android.R;
25+
import com.zulip.android.util.DrawCustomView;
26+
import com.zulip.android.util.PhotoHelper;
27+
28+
import static android.graphics.Bitmap.createBitmap;
29+
30+
public class PhotoEditActivity extends AppCompatActivity {
31+
32+
private String mPhotoPath;
33+
private ImageView mImageView;
34+
private DrawCustomView mDrawCustomView;
35+
private SimpleTarget mGlideTarget;
36+
private int[] mImageDimensions;
37+
38+
@Override
39+
protected void onCreate(Bundle savedInstanceState) {
40+
super.onCreate(savedInstanceState);
41+
setContentView(R.layout.activity_photo_edit);
42+
43+
// set a border and color for black marker color
44+
ImageView black_marker = (ImageView) findViewById(R.id.black_marker);
45+
GradientDrawable blackCircle = (GradientDrawable) black_marker.getDrawable();
46+
blackCircle.setColor(ContextCompat.getColor(this, R.color.black_marker_tool));
47+
blackCircle.setStroke(3, Color.GRAY);
48+
49+
// change background of marker tool to default color red on activity start up
50+
int colorId = R.color.red_marker_tool;
51+
ImageView markerIcon = (ImageView) findViewById(R.id.marker_btn);
52+
GradientDrawable markerBackground = (GradientDrawable) markerIcon.getBackground();
53+
markerBackground.setColor(ContextCompat.getColor(this, colorId));
54+
markerBackground.setStroke(0, Color.GRAY);
55+
56+
// run activity in full screen mode
57+
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
58+
WindowManager.LayoutParams.FLAG_FULLSCREEN);
59+
60+
// get file path of image from PhotoSendActivity
61+
final Intent intent = getIntent();
62+
mPhotoPath = intent.getStringExtra(Intent.EXTRA_TEXT);
63+
64+
mImageView = (ImageView) findViewById(R.id.photoImageView);
65+
mDrawCustomView = (DrawCustomView) findViewById(R.id.draw_custom_view);
66+
67+
// glide target called when image is loaded
68+
Display display = getWindowManager().getDefaultDisplay();
69+
Point size = new Point();
70+
display.getSize(size);
71+
int width = size.x;
72+
int height = size.y;
73+
mGlideTarget = new SimpleTarget<Bitmap>(width, height) {
74+
@Override
75+
public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
76+
// set bitmap on imageView
77+
mImageView.setImageBitmap(bitmap);
78+
79+
// bound the canvas for drawing to the actual dimensions of imageView
80+
mImageDimensions = PhotoHelper.getBitmapPositionInsideImageView(mImageView);
81+
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
82+
mImageDimensions[2],
83+
mImageDimensions[3]
84+
);
85+
params.setMargins(mImageDimensions[0], mImageDimensions[1], 0, 0);
86+
mDrawCustomView.setLayoutParams(params);
87+
}
88+
};
89+
90+
// use glide to take care of high performance bitmap decoding
91+
Glide
92+
.with(this)
93+
.load(mPhotoPath)
94+
.asBitmap()
95+
.into(mGlideTarget);
96+
97+
// set up undo button
98+
ImageView undoBtn = (ImageView) findViewById(R.id.undo_btn);
99+
undoBtn.setOnClickListener(new View.OnClickListener() {
100+
@Override
101+
public void onClick(View view) {
102+
mDrawCustomView.onClickUndo();
103+
}
104+
});
105+
106+
// go back when back button is pressed
107+
ImageView backBtn = (ImageView) findViewById(R.id.back_btn);
108+
backBtn.setOnClickListener(new View.OnClickListener() {
109+
@Override
110+
public void onClick(View view) {
111+
PhotoEditActivity.super.onBackPressed();
112+
}
113+
});
114+
115+
// set up crop button
116+
// intent to go back to PhotoSendActivity
117+
final Intent cropIntent = new Intent(this, PhotoSendActivity.class);
118+
cropIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
119+
TextView cropBtn = (TextView) findViewById(R.id.crop_btn);
120+
cropBtn.setOnClickListener(new View.OnClickListener() {
121+
@Override
122+
public void onClick(View v) {
123+
// pass edited photo file path to PhotoSendActivity
124+
FrameLayout frameLayout = (FrameLayout) findViewById(R.id.frame_layout_picture);
125+
frameLayout.setVisibility(View.INVISIBLE);
126+
127+
// take screenshot of cropped image
128+
if (frameLayout.getWidth() > 0 && frameLayout.getHeight() > 0) {
129+
Bitmap bitmap = screenShot(frameLayout);
130+
mPhotoPath = PhotoHelper.saveBitmapAsFile(mPhotoPath, bitmap);
131+
132+
cropIntent.putExtra(PhotoEditActivity.class.getSimpleName(), true);
133+
cropIntent.putExtra(Intent.EXTRA_TEXT, mPhotoPath);
134+
startActivity(cropIntent);
135+
} else {
136+
// do nothing
137+
// wait for layout to be constructed
138+
}
139+
}
140+
});
141+
142+
// intent to go back to ZulipActivity and upload photo
143+
// when send button is clicked
144+
ImageView sendPhoto = (ImageView) findViewById(R.id.send_photo);
145+
final Intent sendIntent = new Intent(this, ZulipActivity.class);
146+
sendIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
147+
sendPhoto.setOnClickListener(new View.OnClickListener() {
148+
@Override
149+
public void onClick(View view) {
150+
// pass edited photo file path to ZulipActivity
151+
FrameLayout frameLayout = (FrameLayout) findViewById(R.id.frame_layout_picture);
152+
frameLayout.setVisibility(View.INVISIBLE);
153+
154+
// take screenshot of cropped image
155+
Bitmap bitmap = screenShot(frameLayout);
156+
mPhotoPath = PhotoHelper.saveBitmapAsFile(mPhotoPath, bitmap);
157+
158+
sendIntent.putExtra(Intent.EXTRA_TEXT, mPhotoPath);
159+
startActivity(sendIntent);
160+
}
161+
});
162+
}
163+
164+
/**
165+
* This function is called when any of the marker colors are chosen.
166+
* Its sets the color for marker tool and changes its the background.
167+
*
168+
* @param view color ImageView
169+
*/
170+
public void handleMarkerColorChange(View view) {
171+
int colorId = R.color.red_marker_tool;
172+
switch (view.getId()) {
173+
case R.id.red_marker:
174+
colorId = R.color.red_marker_tool;
175+
break;
176+
case R.id.yellow_marker:
177+
colorId = R.color.yellow_marker_tool;
178+
break;
179+
case R.id.green_marker:
180+
colorId = R.color.green_marker_tool;
181+
break;
182+
case R.id.white_marker:
183+
colorId = R.color.white_marker_tool;
184+
break;
185+
case R.id.blue_marker:
186+
colorId = R.color.blue_marker_tool;
187+
break;
188+
case R.id.black_marker:
189+
colorId = R.color.black_marker_tool;
190+
break;
191+
default:
192+
Log.e("Marker Tool", "Invalid color");
193+
break;
194+
}
195+
196+
// change marker tool color
197+
mDrawCustomView.setBrushColor(ContextCompat.getColor(this, colorId));
198+
199+
// change background of marker tool
200+
ImageView markerIcon = (ImageView) findViewById(R.id.marker_btn);
201+
GradientDrawable markerBackground = (GradientDrawable) markerIcon.getBackground();
202+
markerBackground.setColor(ContextCompat.getColor(this, colorId));
203+
// if black color is selected, add a border to the background of marker tool
204+
if (colorId == R.color.black_marker_tool) {
205+
markerBackground.setStroke(3, Color.GRAY);
206+
} else {
207+
markerBackground.setStroke(0, Color.GRAY);
208+
}
209+
}
210+
211+
/**
212+
* Function that takes a screenshot of the view passed and returns a bitmap for it.
213+
*
214+
* @param view {@link View}
215+
* @return screenshot of the view passed
216+
*/
217+
public Bitmap screenShot(View view) {
218+
// Define a bitmap with the same size as the view
219+
Bitmap returnedBitmap = createBitmap(view.getWidth(), view.getHeight(),
220+
Bitmap.Config.RGB_565);
221+
222+
// Bind a canvas to it
223+
Canvas canvas = new Canvas(returnedBitmap);
224+
225+
// Get the view's background
226+
Drawable bgDrawable = view.getBackground();
227+
if (bgDrawable != null)
228+
// has background drawable, then draw it on the canvas
229+
bgDrawable.draw(canvas);
230+
else
231+
// does not have background drawable, then draw black background on
232+
// the canvas
233+
canvas.drawColor(Color.BLACK);
234+
235+
// draw the view on the canvas
236+
view.draw(canvas);
237+
238+
// obtained only the visible region of edited image
239+
Bitmap trimmedBitmap = null;
240+
if (mImageDimensions != null) {
241+
trimmedBitmap = Bitmap.createBitmap(returnedBitmap,
242+
mImageDimensions[0], mImageDimensions[1],
243+
mImageDimensions[2], mImageDimensions[3]);
244+
}
245+
return trimmedBitmap;
246+
}
247+
}

0 commit comments

Comments
 (0)