diff --git a/.gitignore b/.gitignore index afbdab3..70fd93f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,16 @@ +*.iml +*.apk .gradle +/build +/captures /local.properties -/.idea/workspace.xml -/.idea/libraries + +# Intellij IDEA +/.idea + +# Windows +Thumbs.db + +# OS X .DS_Store -/build + diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 4d3d4b8..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -ServiceCamera \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 217af47..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf3..0000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index e206d70..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index fe865d3..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 59436c9..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 4beef45..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml deleted file mode 100644 index 922003b..0000000 --- a/.idea/scopes/scope_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index def6a6a..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/README.txt b/README.md similarity index 58% rename from README.txt rename to README.md index 1619bcd..6d589fa 100644 --- a/README.txt +++ b/README.md @@ -1,3 +1,5 @@ +# ServiceCamera + The recorded video will store in /sdcard/Pictures/MyCameraApp/*.mp4 -The states only show on ADB LOG but not UI. +Now support Start/Stop button. diff --git a/ServiceCamera.iml b/ServiceCamera.iml deleted file mode 100644 index 0bb6048..0000000 --- a/ServiceCamera.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/app.iml b/app/app.iml deleted file mode 100644 index 65f1a32..0000000 --- a/app/app.iml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/build.gradle b/app/build.gradle index 8b28422..d2f767e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,4 +21,5 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.3' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index efd0fa2..245b11f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ + package="example.chatea.servicecamera"> + @@ -12,10 +13,10 @@ + android:label="@string/app_name"> + android:label="@string/app_name"> @@ -23,11 +24,21 @@ + + + - + android:enabled="true"> + + + + + + + - + \ No newline at end of file diff --git a/app/src/main/java/example/chatea/servicecamera/CameraService.java b/app/src/main/java/example/chatea/servicecamera/CameraService.java index 3f3ec24..7e10af8 100644 --- a/app/src/main/java/example/chatea/servicecamera/CameraService.java +++ b/app/src/main/java/example/chatea/servicecamera/CameraService.java @@ -1,14 +1,22 @@ package example.chatea.servicecamera; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; + +import android.graphics.BitmapFactory; import android.graphics.PixelFormat; import android.hardware.Camera; import android.media.CamcorderProfile; import android.media.MediaRecorder; -import android.os.Handler; + +import android.os.Bundle; import android.os.IBinder; +import android.os.ResultReceiver; + import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -21,13 +29,47 @@ import java.util.List; public class CameraService extends Service { + private static final String TAG = CameraService.class.getSimpleName(); + + public static final String RESULT_RECEIVER = "resultReceiver"; + public static final String VIDEO_PATH = "recordedVideoPath"; + public static final int RECORD_RESULT_OK = 0; + public static final int RECORD_RESULT_DEVICE_NO_CAMERA = 1; + public static final int RECORD_RESULT_GET_CAMERA_FAILED = 2; + public static final int RECORD_RESULT_ALREADY_RECORDING = 3; + public static final int RECORD_RESULT_NOT_RECORDING = 4; + + private static final String START_SERVICE_COMMAND = "startServiceCommands"; + private static final int COMMAND_NONE = -1; + private static final int COMMAND_START_RECORDING = 0; + private static final int COMMAND_STOP_RECORDING = 1; + + private int i = 1; private Camera mCamera; private MediaRecorder mMediaRecorder; + private boolean mRecording = false; + private String mRecordingPath = null; + private String mRecordingPathCache = null; + public CameraService() { } + public static void startToStartRecording(Context context, ResultReceiver resultReceiver) { + Intent intent = new Intent(context, CameraService.class); + intent.putExtra(START_SERVICE_COMMAND, COMMAND_START_RECORDING); + intent.putExtra(RESULT_RECEIVER, resultReceiver); + context.startService(intent); + } + + public static void startToStopRecording(Context context, ResultReceiver resultReceiver) { + Intent intent = new Intent(context, CameraService.class); + intent.putExtra(START_SERVICE_COMMAND, COMMAND_STOP_RECORDING); + intent.putExtra(RESULT_RECEIVER, resultReceiver); + context.startService(intent); + } + /** * Used to take picture. */ @@ -52,8 +94,29 @@ public void onPictureTaken(byte[] data, Camera camera) { @Override public int onStartCommand(Intent intent, int flags, int startId) { + switch (intent.getIntExtra(START_SERVICE_COMMAND, COMMAND_NONE)) { + //TODO bug:Sometime intent.getIntExtra is Null + case COMMAND_START_RECORDING: + handleStartRecordingCommand(intent); + break; + case COMMAND_STOP_RECORDING: + handleStopRecordingCommand(intent); + break; + default: + throw new UnsupportedOperationException("Cannot start service with illegal commands"); + } + return START_STICKY; + } + + private void handleStartRecordingCommand(Intent intent) { + final ResultReceiver resultReceiver = intent.getParcelableExtra(RESULT_RECEIVER); - Log.d("TAG", "======= service in onStartCommand"); + if (mRecording) { + // Already recording + resultReceiver.send(RECORD_RESULT_ALREADY_RECORDING, null); + return; + } + mRecording = true; if (Util.checkCameraHardware(this)) { mCamera = Util.getCameraInstance(); @@ -66,13 +129,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, PixelFormat.TRANSLUCENT); - SurfaceHolder sh = sv.getHolder(); sv.setZOrderOnTop(true); sh.setFormat(PixelFormat.TRANSPARENT); - sh.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { @@ -101,7 +162,6 @@ public void surfaceCreated(SurfaceHolder holder) { e.printStackTrace(); } mCamera.startPreview(); -// mCamera.takePicture(null, null, mPicture); // used to takePicture. mCamera.unlock(); @@ -113,30 +173,22 @@ public void surfaceCreated(SurfaceHolder holder) { mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)); - mMediaRecorder.setOutputFile(Util.getOutputMediaFile(Util.MEDIA_TYPE_VIDEO).getPath()); + mRecordingPath = Util.getOutputMediaFile(Util.MEDIA_TYPE_VIDEO).getPath(); + mMediaRecorder.setOutputFile(mRecordingPath); mMediaRecorder.setPreviewDisplay(holder.getSurface()); try { mMediaRecorder.prepare(); } catch (IllegalStateException e) { - Log.d("TAG", "====== IllegalStateException preparing MediaRecorder: " + e.getMessage()); + Log.d(TAG, "IllegalStateException when preparing MediaRecorder: " + e.getMessage()); } catch (IOException e) { - Log.d("TAG", "====== IOException preparing MediaRecorder: " + e.getMessage()); + Log.d(TAG, "IOException when preparing MediaRecorder: " + e.getMessage()); } mMediaRecorder.start(); - Log.d("TAG", "========= recording start"); - - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - mMediaRecorder.stop(); - mMediaRecorder.release(); - mCamera.stopPreview(); - mCamera.release(); - Log.d("TAG", "========== recording finished."); - } - }, 10000); + + resultReceiver.send(RECORD_RESULT_OK, null); + Log.d(TAG, "Recording is started"); } @Override @@ -152,13 +204,72 @@ public void surfaceDestroyed(SurfaceHolder holder) { wm.addView(sv, params); } else { - Log.d("TAG", "==== get Camera from service failed"); + Log.d(TAG, "Get Camera from service failed"); + resultReceiver.send(RECORD_RESULT_GET_CAMERA_FAILED, null); } } else { - Log.d("TAG", "==== There is no camera hardware on device."); + Log.d(TAG, "There is no camera hardware on device."); + resultReceiver.send(RECORD_RESULT_DEVICE_NO_CAMERA, null); + } + } + + private void handleStopRecordingCommand(Intent intent) { + ResultReceiver resultReceiver = intent.getParcelableExtra(RESULT_RECEIVER); + + if (!mRecording) { + // have not recorded + resultReceiver.send(RECORD_RESULT_NOT_RECORDING, null); + return; } - return super.onStartCommand(intent, flags, startId); + mMediaRecorder.stop(); + mMediaRecorder.release(); + mCamera.stopPreview(); + mCamera.release(); + + Bundle b = new Bundle(); + b.putString(VIDEO_PATH, mRecordingPath); + + mRecordingPathCache = mRecordingPath; + mRecordingPath = null; + + resultReceiver.send(RECORD_RESULT_OK, b); + + mRecording = false; + Log.d(TAG, "recording is finished."); + + //Send locall Notification + newHaloMovieReminder(); + } + + private void newHaloMovieReminder() { + try { + NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + Notification.Builder builder = new Notification.Builder(this); + Intent intent = new Intent(this, PhoneCallVideoPlayer.class); + intent.putExtra(VIDEO_PATH, mRecordingPathCache); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + PendingIntent contentIndent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_ONE_SHOT); + builder.setContentIntent(contentIndent) + .setSmallIcon(R.mipmap.ic_launcher) + .setLargeIcon( + BitmapFactory.decodeResource(this.getResources(), + R.mipmap.ic_launcher)) + .setTicker("Halo | PhoneCall Movie") + .setWhen(System.currentTimeMillis()) + .setAutoCancel(true) + .setContentTitle("Halo") + .setContentText("get new PhoneCall Movie"); + Notification notification = builder.getNotification(); + notification.defaults |= Notification.DEFAULT_SOUND; + notification.defaults |= Notification.DEFAULT_LIGHTS; + notificationManager.notify(1, notification); + i++; // for multiple Notification lines + } catch (Exception e) { + Log.d(TAG, "Notification something wrong"); + } } @Override @@ -166,4 +277,5 @@ public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } + } diff --git a/app/src/main/java/example/chatea/servicecamera/MainActivity.java b/app/src/main/java/example/chatea/servicecamera/MainActivity.java index 42e7552..491ef61 100644 --- a/app/src/main/java/example/chatea/servicecamera/MainActivity.java +++ b/app/src/main/java/example/chatea/servicecamera/MainActivity.java @@ -1,14 +1,20 @@ package example.chatea.servicecamera; import android.app.Activity; -import android.content.Intent; import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; import android.view.View; import android.widget.Button; +import android.widget.Toast; - +/** + * TODO add keyguard when recording. (Cannot leave app when it is recording) + */ public class MainActivity extends Activity { + private boolean mRecording; + private Button bt_recordingButton; @Override @@ -20,13 +26,68 @@ protected void onCreate(Bundle savedInstanceState) { bt_recordingButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - startRecording(); + if (mRecording) { + stopRecording(); + } else { + startRecording(); + } } }); } private void startRecording() { - Intent intent = new Intent(this, CameraService.class); - startService(intent); + setRecording(true); + + ResultReceiver receiver = new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + handleStartRecordingResult(resultCode, resultData); + } + }; + + CameraService.startToStartRecording(this, receiver); + } + + private void stopRecording() { + setRecording(false); + + ResultReceiver receiver = new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + handleStopRecordingResult(resultCode, resultData); + } + }; + + CameraService.startToStopRecording(this, receiver); + } + + private void setRecording(boolean recording) { + if (recording) { + mRecording = true; + bt_recordingButton.setText(R.string.stop_recording); + } else { + mRecording = false; + bt_recordingButton.setText(R.string.start_recording); + } + } + + private void handleStartRecordingResult(int resultCode, Bundle resultData) { + if (resultCode == CameraService.RECORD_RESULT_OK) { + Toast.makeText(this, "Start recording...", Toast.LENGTH_SHORT).show(); + } else { + // start recording failed. + Toast.makeText(this, "Start recording failed...", Toast.LENGTH_SHORT).show(); + setRecording(false); + } + } + + private void handleStopRecordingResult(int resultCode, Bundle resultData) { + if (resultCode == CameraService.RECORD_RESULT_OK) { + String videoPath = resultData.getString(CameraService.VIDEO_PATH); + Toast.makeText(this, "Record succeed, file saved in " + videoPath, + Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(this, "Record failed...", Toast.LENGTH_SHORT).show(); + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/example/chatea/servicecamera/PhoneCallVideoPlayer.java b/app/src/main/java/example/chatea/servicecamera/PhoneCallVideoPlayer.java new file mode 100644 index 0000000..805b991 --- /dev/null +++ b/app/src/main/java/example/chatea/servicecamera/PhoneCallVideoPlayer.java @@ -0,0 +1,29 @@ +package example.chatea.servicecamera; + +import android.content.Intent; +import android.net.Uri; +import android.support.v7.app.ActionBarActivity; +import android.os.Bundle; +import android.util.Log; +import android.widget.MediaController; +import android.widget.VideoView; + +public class PhoneCallVideoPlayer extends ActionBarActivity { + + private VideoView mVideoView; + private static final String TAG = PhoneCallVideoPlayer.class.getSimpleName();; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.phone_call_video_player); + mVideoView = (VideoView) findViewById(R.id.my_videoview); + Intent intent = this.getIntent(); + String mRecordingPath = intent.getStringExtra(CameraService.VIDEO_PATH); + Log.i(TAG, mRecordingPath); + mVideoView.setVideoURI(Uri.parse(mRecordingPath)); + mVideoView.setMediaController(new MediaController(this)); + mVideoView.requestFocus(); + mVideoView.start(); + } +} diff --git a/app/src/main/java/example/chatea/servicecamera/PhonecallReceiver.java b/app/src/main/java/example/chatea/servicecamera/PhonecallReceiver.java new file mode 100644 index 0000000..c1354a7 --- /dev/null +++ b/app/src/main/java/example/chatea/servicecamera/PhonecallReceiver.java @@ -0,0 +1,156 @@ +package example.chatea.servicecamera; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.widget.Toast; + +import java.util.Date; + +public class PhoneCallReceiver extends BroadcastReceiver { + + //The receiver will be recreated whenever android feels like it. We need a static variable to remember data between instantiations + + private static int lastState = TelephonyManager.CALL_STATE_IDLE; + private static Date callStartTime; + private static boolean isIncoming; + private static String savedNumber; //because the passed incoming is only valid in ringing + private static String TAG = "PhoneCallReceiver"; + private boolean mRecording; + + + @Override + public void onReceive(Context context, Intent intent) { + + //We listen to two intents. The new outgoing call only tells us of an outgoing call. We use it to get the number. + if (intent.getAction().equals("android.intent.action.NEW_OUTGOING_CALL")) { + savedNumber = intent.getExtras().getString("android.intent.extra.PHONE_NUMBER"); + Log.d(TAG, "NEW_OUTGOING_CALL"); + } else { + String stateStr = intent.getExtras().getString(TelephonyManager.EXTRA_STATE); + String number = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER); + Log.d(TAG, stateStr + " with " + number); + int state = 0; + if (stateStr.equals(TelephonyManager.EXTRA_STATE_IDLE)) { + state = TelephonyManager.CALL_STATE_IDLE; + } else if (stateStr.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) { + state = TelephonyManager.CALL_STATE_OFFHOOK; + } else if (stateStr.equals(TelephonyManager.EXTRA_STATE_RINGING)) { + state = TelephonyManager.CALL_STATE_RINGING; + } + onCallStateChanged(context, state, number); + } + } + + //Derived classes should override these to respond to specific events of interest + protected void onIncomingCallStarted(Context ctx, String number, Date start) { + tryToRecording(ctx); + } + + protected void onOutgoingCallStarted(Context ctx, String number, Date start) { + tryToRecording(ctx); + } + + protected void onIncomingCallEnded(Context ctx, String number, Date start, Date end) { + tryToStopRecording(ctx); + } + + protected void onOutgoingCallEnded(Context ctx, String number, Date start, Date end) { + tryToStopRecording(ctx); + } + + protected void onMissedCall(Context ctx, String number, Date start) { + tryToStopRecording(ctx); + } + + //Deals with actual events + + //Incoming call- goes from IDLE to RINGING when it rings, to OFFHOOK when it's answered, to IDLE when its hung up + //Outgoing call- goes from IDLE to OFFHOOK when it dials out, to IDLE when hung up + public void onCallStateChanged(Context context, int state, String number) { + if (lastState == state) { + //No change, debounce extras + return; + } + switch (state) { + case TelephonyManager.CALL_STATE_RINGING: + isIncoming = true; + callStartTime = new Date(); + savedNumber = number; + onIncomingCallStarted(context, number, callStartTime); + Log.d(TAG, "CALL_STATE_RINGING"); + break; + case TelephonyManager.CALL_STATE_OFFHOOK: + //Transition of ringing->offhook are pickups of incoming calls. Nothing done on them + if (lastState != TelephonyManager.CALL_STATE_RINGING) { + isIncoming = false; + callStartTime = new Date(); + onOutgoingCallStarted(context, savedNumber, callStartTime); + Log.d(TAG, "CALL_STATE_OFFHOOK"); + } + break; + case TelephonyManager.CALL_STATE_IDLE: + Log.d(TAG, "CALL_STATE_IDLE"); + //Went to idle- this is the end of a call. What type depends on previous state(s) + if (lastState == TelephonyManager.CALL_STATE_RINGING) { + //Ring but no pickup- a miss + onMissedCall(context, savedNumber, callStartTime); + } else if (isIncoming) { + onIncomingCallEnded(context, savedNumber, callStartTime, new Date()); + } else { + onOutgoingCallEnded(context, savedNumber, callStartTime, new Date()); + } + Log.d("tryToStopRecording", "mRecording =" + String.valueOf(mRecording)); + break; + } + lastState = state; + } + + private void setRecording(boolean recording) { + mRecording = recording; + } + + private void tryToRecording(Context context) { + if (mRecording) { + Toast.makeText(context, "Already recording...", Toast.LENGTH_SHORT).show(); + return; + } + startRecording(context); + } + + private void tryToStopRecording(Context context) { + if (!mRecording) { + stopRecording(context); + Toast.makeText(context, "stop recording...", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(context, "recording?" + String.valueOf(mRecording), Toast.LENGTH_SHORT).show(); + } + } + + private void startRecording(Context context) { + setRecording(true); + ResultReceiver receiver = new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + } + }; + CameraService.startToStartRecording(context, receiver); + } + + private void stopRecording(Context context) { + setRecording(false); + ResultReceiver receiver = new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + } + }; + CameraService.startToStopRecording(context, receiver); + } + + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 7174930..19f60e6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,7 +1,8 @@ - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/start_recording" /> - + diff --git a/app/src/main/res/layout/phone_call_video_player.xml b/app/src/main/res/layout/phone_call_video_player.xml new file mode 100644 index 0000000..4f480ca --- /dev/null +++ b/app/src/main/res/layout/phone_call_video_player.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e485277..bf3e0c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,4 +3,7 @@ Hello world! Settings + + Start Recording + Stop Recording diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e76..05cea2b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Mon Apr 11 21:59:28 CST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip